QEMU:模拟 ARM 大端字节序运行环境

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. ARM 大小端模拟测试

本文通过 VMware + Ubuntu + QEMU 进行测试验证,有需要的读者可以先行构建测试环境。

2.1 裸机模拟测试

2.1.1 大端模拟测试

测试程序 endian_test_system_assign.S 汇编代码:

	.text
	
	.global _start
	
_start:
	@ setup SP pointer
	mov sp, #0x60000000
	add sp, sp, #12
	
	@ u16 = 0x1234
	movw r0, #0x1234
	strh r0, [sp, #-6]
	
	@ u8 = u16
	ldrh r1, [sp, #-6]
	strb r1, [sp, #-7]
	
	@ read u8
	mov r3, #0
	ldrb r3, [sp, #-7]
	
	@ read u16
	mov r4, #0
	ldrh r4, [sp, #-6]

	@ read u32
	mov r5, #0
	ldr r5, [sp, #-8]

1:
	b 1b

这段汇编的代码的核心逻辑,是将一个 u16 类型强制赋值给一个 u8,然后读取 u8 的值,看 u8 的值是 u16高 8-bit 还是低 8-bit,即:

// ? 将代码编译为大端程序后,在 ARM 大端模式机器上运行,u8_var 的值是 0x34,还是 0x12 ?
u16 u16_var = 0x1234;
u8 u8_var = u16_var;

先下载支持大端编译的 ARM 交叉编译器

https://releases.linaro.org/components/toolchain/binaries/latest-7/armeb-eabi/

然后编译:

$ armeb-eabi-gcc -nostdlib -g -march=armv7-a -mbig-endian -o endian_test_system_assign.elf endian_test_system_assign.S
$ file endian_test_system_assign.elf
endian_test_system_assign.elf: ELF 32-bit MSB executable, ARM, EABI5 BE8 version 1 (SYSV), statically linked, BuildID[sha1]=7e5a4b0f93d2b1d66514c87d831881601bdd7efc, with debug_info, not stripped

file 命令的输出,其中的 MSB 标识编译出来的为大端程序

QEMU 会判定程序的大小端,然后将 CPU 设置为程序要求的大小端模式。用 QEMU 模拟大端程序的运行:

$ qemu-system-arm -M vexpress-a9 -m 256M -kernel endian_test_system_assign.elf -nographic -d in_asm,cpu,int,exec
pulseaudio: set_sink_input_volume() failed
pulseaudio: Reason: Invalid argument
pulseaudio: set_sink_input_mute() failed
pulseaudio: Reason: Invalid argument
----------------
IN: 
0x00008024:  e3a0d206      mov sp, #1610612736 ; 0x60000000
0x00008028:  e28dd00c      add sp, sp, #12 ; 0xc
0x0000802c:  e3010234      movw r0, #4660 ; 0x1234
0x00008030:  e14d00b6      strh r0, [sp, #-6]
0x00008034:  e15d10b6      ldrh r1, [sp, #-6]
0x00008038:  e54d1007      strb r1, [sp, #-7]
0x0000803c:  e3a03000      mov r3, #0 ; 0x0
0x00008040:  e55d3007      ldrb r3, [sp, #-7]
0x00008044:  e3a04000      mov r4, #0 ; 0x0
0x00008048:  e15d40b6      ldrh r4, [sp, #-6]
0x0000804c:  e3a05000      mov r5, #0 ; 0x0
0x00008050:  e51d5008      ldr r5, [sp, #-8]
0x00008054:  eafffffe      b 0x8054

Trace 0x7fd88b3160c0 [0: 00008024] 
R00=00000000 R01=00000000 R02=00000000 R03=00000000
R04=00000000 R05=00000000 R06=00000000 R07=00000000
R08=00000000 R09=00000000 R10=00000000 R11=00000000
R12=00000000 R13=00000000 R14=00000000 R15=00008024
PSR=400003d3 -Z-- A S svc32
----------------
IN: 
0x00008054:  eafffffe      b 0x8054

Linking TBs 0x7fd88b3160c0 [00008024] index 0 -> 0x7fd88b316400 [00008054]
Trace 0x7fd88b316400 [0: 00008054] 
R00=00001234 R01=00001234 R02=00000000 R03=00000034
R04=00001234 R05=00341234 R06=00000000 R07=00000000
R08=00000000 R09=00000000 R10=00000000 R11=00000000
R12=00000000 R13=6000000c R14=00000000 R15=00008054
PSR=400003d3 -Z-- A S svc32
Linking TBs 0x7fd88b316400 [00008054] index 0 -> 0x7fd88b316400 [00008054]
Trace 0x7fd88b316400 [0: 00008054] 
R00=00001234 R01=00001234 R02=00000000 R03=00000034
R04=00001234 R05=00341234 R06=00000000 R07=00000000
R08=00000000 R09=00000000 R10=00000000 R11=00000000
R12=00000000 R13=6000000c R14=00000000 R15=00008054
PSR=400003d3 -Z-- A S svc32

通过 QEMU 模拟器的 -d in_asm,cpu,int,exec 选项,输出指令执行后的寄存器值。我们这里主要观察 PSR,R0,R1,R3,R4,R5 这几个寄存器的输出值:

. PSR=400003d3,bit 91,表示 CPU 处于大端模式;
. R00=00001234 R01=00001234:表示成功对堆栈一个 u16 空间写入、读取;
. R03=00000034:表示对堆栈一个 u8 空间成功写入。

同时:

R03=00000034 R04=00001234 R05=00341234

反映出在大端机器上,程序代码的 u16u8 变量读写后内存空间布局如下:

在这里插入图片描述
从以上的测试可以了解到,在 ARM 大端模式的机器上,代码片段:

// ? 将代码编译为大端程序后,在 ARM 大端模式机器上运行,u8_var 的值是 0x34,还是 0x12 ?
u16 u16_var = 0x1234;
u8 u8_var = u16_var;

最后 u8_var 的值为 0x34(即上面测试验证中寄存器 R3 的值),这表示:不同类型间的直接赋值操作,其结果是由语言语义定义的,和机器的大小端无关,即不管是在大端机器上运行,还是在小端机器上运行,总是会得到相同的、由语言语义定义的结果

前面讨论的情形是类型间的直接赋值,那如果使用指针方式,结果将会怎样?假设有如下代码片段:

// ? 将代码编译为大端程序后,在 ARM 大端模式机器上运行,u8_var 的值是 0x34,还是 0x12 ?
unsigned short u16_var = 0x1234;
unsigned char u8_var = *((unsigned char *)&u16_var);

u8_var 的值最后会是多少?我们将上面的代码片段转换为如下 ARM 汇编代码 endian_test_system_pointer.S ,并进行裸机测试,看看结果如何。

	.text

	.global _start

_start:
	@ setup SP pointer
	mov	sp, #0x60000000
	add	sp, sp, #12

	@ unsigned short u16_var = 0x1234;
	ldr	r1, .Lword_var
	strh	r1, [sp, #-8]
	@ unsigned char u8_var = *((unsigned char *)&u16_var);
	sub	r2, sp, #8
	ldrb	r3, [r2]
	strb	r3, [sp, #-5]
	nop

	@ read u8_var
	mov	r4, #0
	ldrb	r4, [sp, #-5]
	@ read u32
	mov	r5, #0
	ldrh	r5, [sp, #-8]

1:
	b 1b

.Lword_var:
	.word	0x1234

安装 ARM 交叉编译小端编译器,并使用小端编译器进行编译

$ sudo apt-get install gcc-arm-linux-gnueabihf
$ arm-linux-gnueabihf-gcc -nostdlib -g -march=armv7-a -mbig-endian -o endian_test_system_pointer.elf endian_test_system_pointer.S

运行测试:

$ qemu-system-arm -M vexpress-a9 -m 256M -kernel endian_test_system_pointer.elf -nographic -d in_asm,cpu,int,exec
pulseaudio: set_sink_input_volume() failed
pulseaudio: Reason: Invalid argument
pulseaudio: set_sink_input_mute() failed
pulseaudio: Reason: Invalid argument
----------------
IN: 
0x00008024:  e3a0d206      mov	sp, #1610612736	; 0x60000000
0x00008028:  e28dd00c      add	sp, sp, #12	; 0xc
0x0000802c:  e59f1024      ldr	r1, [pc, #36]	; 0x8058
0x00008030:  e14d10b8      strh	r1, [sp, #-8]
0x00008034:  e24d2008      sub	r2, sp, #8	; 0x8
0x00008038:  e5d23000      ldrb	r3, [r2]
0x0000803c:  e54d3005      strb	r3, [sp, #-5]
0x00008040:  e320f000      nop	{0}
0x00008044:  e3a04000      mov	r4, #0	; 0x0
0x00008048:  e55d4005      ldrb	r4, [sp, #-5]
0x0000804c:  e3a05000      mov	r5, #0	; 0x0
0x00008050:  e15d50b8      ldrh	r5, [sp, #-8]
0x00008054:  eafffffe      b	0x8054

Trace 0x7fbf5adc80c0 [0: 00008024] 
R00=00000000 R01=00000000 R02=00000000 R03=00000000
R04=00000000 R05=00000000 R06=00000000 R07=00000000
R08=00000000 R09=00000000 R10=00000000 R11=00000000
R12=00000000 R13=00000000 R14=00000000 R15=00008024
PSR=400003d3 -Z-- A S svc32
----------------
IN: 
0x00008054:  eafffffe      b	0x8054

Linking TBs 0x7fbf5adc80c0 [00008024] index 0 -> 0x7fbf5adc8400 [00008054]
Trace 0x7fbf5adc8400 [0: 00008054] 
R00=00000000 R01=00001234 R02=60000004 R03=00000012
R04=00000012 R05=00001234 R06=00000000 R07=00000000
R08=00000000 R09=00000000 R10=00000000 R11=00000000
R12=00000000 R13=6000000c R14=00000000 R15=00008054
PSR=400003d3 -Z-- A S svc32
Linking TBs 0x7fbf5adc8400 [00008054] index 0 -> 0x7fbf5adc8400 [00008054]
Trace 0x7fbf5adc8400 [0: 00008054] 
R00=00000000 R01=00001234 R02=60000004 R03=00000012
R04=00000012 R05=00001234 R06=00000000 R07=00000000
R08=00000000 R09=00000000 R10=00000000 R11=00000000
R12=00000000 R13=6000000c R14=00000000 R15=00008054
PSR=400003d3 -Z-- A S svc32

汇编代码最后将 u8_var 的值加载到了寄存器 R3

R03=00000012

可以看到 R3 寄存器的值为 0x12,即 u8_var 的值为 0x12,这个测试结果不同于前面直接赋值的情形。对于使用指针进行赋值的情形,是将长类型 u16 变量的低地址字节存储的值,赋给了 u8 变量,由于大端字节序低地址存储的是高位数据,所以结果为 0x12。因此,在使用指针赋值时,要想在大小端机器上得到相同的结果,需要做不同的处理,处理方式类似如下伪代码

unsigned short u16_var = 0x1234;
unsigned char u8_var, *u8_var_ptr;

u8_var_ptr = (unsigned char *)&u16_var;
if (是大端机器)
	u8_var = *(u8_var_ptr + 1);
else // 小端机器
	u8_var = *u8_var_ptr;

2.1.2 小端模拟测试

小端裸机测试的结果:不管是直接赋值,还是指针访问,u8 变量得到的结果都是 0x12。本文不对小端裸机测试做展开,感兴趣的读者可自行研究。

2.2 用户空间模拟测试

2.2.1 大端模拟测试

本小节进行用户空间程序的大端模拟测试,编写代码文件 endian_test_user.c

#include <stdio.h>

int main(void)
{
	unsigned short u16_var = 0x1234;
	unsigned char u8_var, *u8_var_p;

	u8_var = u16_var;
	printf("u8_var = 0x%02x\n", u8_var);

	u8_var_p = (unsigned char *)&u16_var;
	printf("*u8_var_p = 0x%02x\n", *u8_var_p);

	return 0;
}

使用 2.1.1 小节下载的 ARM 大端交叉编译器进行编译:

$ armeb-eabi-gcc -static -mbig-endian -o endian_test_user endian_test_user.c
$ file endian_test_user
endian_test_user: ELF 32-bit MSB executable, ARM, EABI5 version 1 (SYSV), statically linked, BuildID[sha1]=d30ccd867632029b8b42592da509d43a7ce35041, with debug_info, not stripped

file 命令的输出,其中的 MSB 标识编译出来的为大端程序

安装 ARM 用户空间程序运行环境模拟器程序 qemu-user-static,并运行测试程序,进行用户空间程序大端模拟测试

$ sudo apt-get install qemu-user-static
$ qemu-armeb-static endian_test_user
u8_var = 0x34
*u8_var_p = 0x12

可见,在大端模式下,使用直接赋值方式u8_var 得到的值为 0x34;使用指针方式u8_var 得到的值是 0x12,这和裸机大端模拟测试的结果一致。

2.2.2 小端模拟测试

本小节进行用户空间程序的小端模拟测试,编写代码文件同 2.2.1 小节的 endian_test_user.c,使用前面安装的 ARM 交叉编译小端编译器 gcc-arm-linux-gnueabihf 进行编译:

$ arm-linux-gnueabihf-gcc -static -mlittle-endian -o endian_test_user endian_test_user.c
$ file endian_test_user
endian_test_user: ELF 32-bit LSB executable, ARM, EABI5 version 1 (GNU/Linux), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=de9c3a8a02cff662db8fea1a660945e82932a446, not stripped

file 命令的输出,其中的 LSB 标识编译出来的为小端程序

运行测试程序:

$ qemu-arm-static endian_test_user
u8_var = 0x34
*u8_var_p = 0x34

从结果看到,在小端模式下,不管是用直接赋值方式,还是指针方式u8_var 得到的值都是 0x34,这和裸机大端模拟测试的结果一致。

2.3 结论

通过前面的大小端模拟测试,我们得出结论:

  • 使用直接赋值方式,将长类型赋值给短类型,总是取长类型低位值给短类型其结果由语言语义定义的,和机器的大小端无关
  • 使用指针赋值方式,将长类型赋值给短类型,总是取长类型低地址字节的值给短类型。这是由机器的存储和 CPU 访存方式决定的

另外,在多字节的赋值中,还应该注意大端字节序的 BE8BE32 不同,更多关于这方面的细节,可参考链接:

https://developer.arm.com/documentation/ddi0290/g/unaligned-and-mixed-endian-data-access-support/mixed-endian-access-support/differences-between-be-32-and-be-8-buses

3. 参考链接

[1] https://developer.arm.com/documentation/ddi0290/g/unaligned-and-mixed-endian-data-access-support/mixed-endian-access-support/differences-between-be-32-and-be-8-buses
[2] https://github.com/pcrost/arm-be-test
[3] https://community.arm.com/support-forums/f/compilers-and-libraries-forum/49616/latest-arm-gcc-compiler-for-big-endian-processors

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值