当endianes确实很重要时,进行强制转换操作

when endianess does matter - cast operations

本文关键字:操作 转换 endianes      更新时间:2023-10-16

可能的重复:
Endianness何时成为一个因素?

阅读这篇关于endianes的教程,我会发现这个例子中endianes确实很重要。这是关于写一个充满1和0的字符*。然后可以将其转换为short,结果取决于endianes,little或big。下面是引用的例子。

无符号char endian[2]={1,0};短x;

x = *(short *) endian;

x的值是多少?让我们看看这个代码在做什么。您正在创建一个由两个字节组成的数组,然后将该数组强制转换为两个字节组成一个短。通过使用数组,您基本上强制一个特定的字节顺序,您将看到系统如何处理这两个字节。如果这是一个小端序系统,则0和1是向后解释,并将其视为0,1。由于高字节是0,这无关紧要,低字节是1,所以x等于1。关于另一方面,如果是big-endian系统,则高字节为1x的值为256。

我想知道:当你用给定数量的内存字节分配(这里是两个字节)实例化一个数组时,只要数组已经分配了与这个字节对应的字节数量,如何转换为任何类型(short,int…)?如果没有分配足够的内存来"包含此类型",下一个内存地址还会被读取吗?例如,如果我想将endian强制转换为long,会执行此操作,从endian的开头读取四个字节,还是会失败?

然后,关于字节序的一个问题是:这是处理器在内存中写入字节的习惯,其中最有意义的字节位于最低内存位置(big-endian)或最高内存位置(little-endian)。在这种情况下,已经分配了一个具有两个单字节元素的数组。为什么说1是最有意义的字节?

不要忘记编译器只会编写汇编代码。如果忽略编译器发出的所有警告,则可以检查编译器生成的程序集代码,并了解实际情况。

我采用了这个简单的程序:

#include <iostream>
int main()
{
unsigned endian[2] = { 0, 0 } ;
long * casted_endian = reinterpret_cast<long*>( endian );
std::cout << *casted_endian << std::endl;
}

并且我使用CCD_ 1提取该代码。让我们破译一下。

804879c:   55                      push   %ebp
804879d:   89 e5                   mov    %esp,%ebp
804879f:   83 e4 f0                and    $0xfffffff0,%esp
80487a2:   83 ec 20                sub    $0x20,%esp

这些行只是函数的序言,忽略它们。

unsigned endian[2] = { 0, 0 } ;
80487a5:   c7 44 24 14 00 00 00    movl   $0x0,0x14(%esp)
80487ac:   00 
80487ad:   c7 44 24 18 00 00 00    movl   $0x0,0x18(%esp)
80487b4:   00 

从这两行中,您可以看到(0x14)%esp是用0初始化的。因此,您知道数组endian在堆栈上,位于寄存器%ESP(堆栈指针)+0x14中的地址。

long * casted_endian = reinterpret_cast<long*>( endian );
80487b5:   8d 44 24 14             lea    0x14(%esp),%eax

LEA只是一种算术运算。EAX现在包含%ESP+0x14,这是堆栈上数组的地址。

80487b9:   89 44 24 1c             mov    %eax,0x1c(%esp)

在地址ESP+0x1c(这是变量casted_endian的位置),我们放置EAX,因此是endian的第一个字节的地址。

std::cout << *casted_endian << std::endl;
80487bd:   8b 44 24 1c             mov    0x1c(%esp),%eax
80487c1:   8b 00                   mov    (%eax),%eax
80487c3:   89 44 24 04             mov    %eax,0x4(%esp)
80487c7:   c7 04 24 40 a0 04 08    movl   $0x804a040,(%esp)
80487ce:   e8 1d fe ff ff          call   80485f0 <std::ostream::operator<<(long)@plt>

然后,我们准备对运营商<lt;与相关的论点没有任何更多的检查。就这样,程序不会再进行任何检查了。变量的类型与机器完全无关。

现在,当operator<<读取*casted_endian中不在数组中的部分时,可能会发生两件事。

它的地址在当前映射的内存页中,或者没有。在第一种情况下,operator<<会毫无怨言地读取该地址的任何内容。这可能会在屏幕上写一些奇怪的东西。在第二种情况下,您的操作系统会抱怨程序试图读取他无法读取的内容,并引发中断。这就是著名的分割错误。

如果您试图强制转换到大于数组的大小,则会得到未定义的行为。它可能会尝试读取数组后面的内存内容,但结果不能保证,也不需要一致。

天啊。我在这里要说的是,为什么这适用于大多数架构,但我不能说其中有多少是标准的。

您正在做的是将数组endian强制转换为short。现在,数组基本上是指针,数组的名称实际上包含第一个元素的地址。唯一真正的区别是数组包含更有用的元数据,并且数组上的某些操作不同(例如sizeof)。然后使用该地址(endian)并从中创建一个objdump0指针。内存地址保持不变,只是您对指向的数据进行了不同的解释。然后,您将取消引用该指针以获取值,并将其分配给x

简短的旁注。这可能不适用于所有系统。在C中,int仅定义为与体系结构的本机字大小一样宽(x86上为4字节,x86_64上为8字节)。short则仅被定义为短于int(或者等于,如果内存服务正确的话)。因此,该代码将在8位体系结构上失败。要实现此操作,目标数据类型的大小(以字节为单位)必须等于或小于数组的大小。

同样,long只是被定义为比int长,通常在x86和x86_64上分别为8或16个字节。在这种情况下,此代码将在x86:上工作

unsigned char endian[8] = {1,2,3,4,5,6,7,8};
long x = *(long*)endian;

无论如何,处理器的字节序完全取决于处理器。x86是little-endian(并且基本上启动了LE设备的约定IIRC)。SPARC是big-endian(直到9,两者都可以)。ARM和MIPS也是可配置的,Microblaze取决于使用的总线(AXI或PLB)。无论如何,字节序不仅限于处理器,在与硬件或其他计算机通信时也是一个问题。

对于最后一个问题,最高有效字节被称为,因为所代表的值大于较小字节所能代表的最大值。在16位字的情况下,最低有效字节可以表示0-255,最高有效字节可以代表256-65535。

在任何情况下,除非您正在进行低级别系统编程(我的意思是,直接修改内存)或编写通信协议,否则您永远不需要担心endianness。

unsigned char endian[2] = {1, 0};
short x;
x = *(short *) endian;

此代码具有未定义的行为。结果可能是x设置为12564000,或者程序可能崩溃,或者其他任何事情都可能合法发生。即使不考虑数组是否足够大以适应它所转换的类型,情况也是如此

以下是对代码的改写,使其合法化,并按照作者的意图行事。

unsigned char endian[sizeof(short)] = {1};
short x;
std::memcpy(&x, endian, sizeof(short));

如果您编写的代码试图从该数组中获取int,那么它将访问合法数组边界之外的内容,并且您将再次遇到未定义的行为;任何事情都有可能发生。

在这种情况下,已经分配了一个具有两个一字节元素的数组。为什么说1是最有意义的字节?

(我猜你是想问为什么endian[1]被认为包含最高有效字节。)

因为在这个例子中,系统是小端序,正如你所说,小端序的定义是内存位置中具有最高地址的最高有效字节。CCD_ 19具有比CCD_ 20更高的地址,因此CCD_。