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