也谈大端模式(big endian)和小端模式(little endian) (二) 理论与实例一

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上的汇编代码:

也谈大端模式(big endian)和小端模式(little endian) (二) 理论与实例一_第1张图片

 

    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

你可能感兴趣的:(也谈大端模式(big endian)和小端模式(little endian) (二) 理论与实例一)