static union
{
char c[4];
unsigned long l;
} endian_test = { { 'l', '?', '?', 'b' } };
#define ENDIANNESS ((char)endian_test.l)
我们可以在Linux内核源码中找到以上代码,这个宏的意图显而易见:小端机器输出 l (little endian), 大端机器输出 b(big endian)。我们分别在PC机和PowerPC的机器上来试验一下:
#include <stdio.h>
static union
{
char c[4];
unsigned long l;
}endian_test = { { 'l','?','?','b' } };
#define ENDIANNESS ((char)endian_test.l)
int main()
{
printf("ENDIANNESS = %c\n", ENDIANNESS);
printf("addr of c[0] = 0x%x, c[3] = 0x%x, l = 0x%x\n", &endian_test.c[0], &endian_test.c[3], &endian_test.l);
return 0;
}
PC:
ENDIANNESS = l
addr of c[0] = 0x8049714, c[3] = 0x8049717, l = 0x8049714
PowerPC:
ENDIANNESS = b
addr of c[0] = 0x7ff18f38, c[3] = 0x7ff18f3b, l = 0x7ff18f38
结果不言而喻,但这不是我们的目的。我们注意到,不管是小端的PC机还是大端的PowerPC,联合体中c[0]的地址都是跟变量 l 的地址是一样的,并且都是在低地址。这也符合“联合体union成员的存放顺序是从低地址开始的”这一描述(注意这句话是不区分大小端的机器的,也就是说不论大端小端机器,这句话都是成立的)。
我们继续分析,根据endian_test的赋值方式,以及上面给出的c[0]的地址,显然不管大端还是小端,c[0]中存放应该都是字符 'l'。但是对于宏ENDIANNESS,大端和小端的机器的结果却不一样,这又是为什么呢?我们还是来看下汇编代码吧:
PC上的汇编代码:
$ gcc -S test-endian.c
$ cat test-endian.s
.file "test-endian.c"
.data
.align 4
.type endian_test, @object
.size endian_test, 4
endian_test:
.byte 108
.byte 63
.byte 63
.byte 98
.section .rodata
.LC0:
.string "ENDIANNESS = %c\n"
.align 4
.LC1:
.string "addr of c[0] = 0x%x, c[3] = 0x%x, l = 0x%x\n"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
andl $-16, %esp
subl $16, %esp
movl endian_test, %eax
movsbl %al, %eax
movl %eax, 4(%esp)
movl $.LC0, (%esp)
call printf
movl $endian_test, 12(%esp)
movl $endian_test+3, 8(%esp)
movl $endian_test, 4(%esp)
movl $.LC1, (%esp)
call printf
movl $0, %eax
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (GNU) 4.7.0 20120507 (Red Hat 4.7.0-5)"
.section .note.GNU-stack,"",@progbits
我们注意到这里:
endian_test:
.byte 108
.byte 63
.byte 63
.byte 98
.section rodata
容易看出,这就是endian_test的初始值, 'l','?','?','b' 对应的ASCII码。接下来就是:
movl endian_test, %eax
movsbl %al, %eax
movl %eax, 4(%esp)
movl $.LC0, (%esp)
call printf
这段代码也比较简单,关键就是movl endian_test, %eax和movsbl %al, %eax 这两句,将endian_test位置取得的四个字节中的最低8位放到寄存器eax中,显然这个值应该是108,即 'l'。剩下的就是给 printf 准备参数。
PowerPC上的汇编代码:
PowerPC的汇编有点特殊,这里不再详述,有兴趣的同学可以查阅参考资料中的相关内容。可以看到,开始的四个字节跟PC上的汇编是一样的,都是存放endian_test的初始值,并且顺序也是一样的。下面来看关键的部分:
lis 9, endian_test@ha
lwz 0, endian_test@l(9) /* 与上面一句合起来表示从 endian_test 处读取一个32位数放到寄存器0中 */
rlwinm 0, 0, 0, 0xff /* 只保留寄存器0的最低8位 */
lis 9, .LC0@ha
la 3, .LC0@l(9)
mr 4, 0 /* 参考上面PC的汇编可知这三句也是给 printf 准备参数*/
bl printf
显然,问题的关键就是 movl endian_test, %eax 和 lwz 0, endian_test@l(9),执行完这两句后,我们可以想象,PC的寄存器eax和PowerPC的寄存器0的值一定是相反的。 对应《也谈大端模式(big endian)和小端模式(little endian) (一) 》中的内容,对于同样一个32位数字如0x11223344,该值在大端和小端机器在内存中的存放顺序不同,但当CPU按照32位读出时,值都是0x11223344;而这里的情况是由于用到了联合体union,所以大端和小端的endian_test在内存中的存放顺序相同,当然结果也正好是取出的值是不同的。注意这里的不同是CPU的行为不同,也就是说,内存中同样的一个数字(指存储顺序一样的情况),大小端按照32位读取到寄存器中的时候,值恰好是相反的。而这只是在寄存器中的值发生了改变,内存中的值是没有变化的,因为只是读取数据嘛。
从另一个角度说,无论对于大端和小端的机器,c[0]的值都是字符 'l',也证明了这种情况下的存放顺序是一样的。存放顺序相同,按四字节取出的话,大小端的机器值当然是不一样的。
参考资料:
【1】Linux PowerPC详解:核心篇
【2】PowerPC 汇编简介 http://www.ibm.com/developerworks/cn/linux/hardware/ppc/assembly/index.html
【3】PowerPC rlwinm指令的运算过程 http://blog.chinaunix.net/uid-361890-id-1564927.html