文章目录
- 1. 前言
- 2. ARM 大小端模拟测试
- 2.1 裸机模拟测试
- 2.1.1 大端模拟测试
- 2.1.2 小端模拟测试
- 2.2 用户空间模拟测试
- 2.2.1 大端模拟测试
- 2.2.2 小端模拟测试
- 2.3 结论
- 3. 参考链接
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 9 为 1,表示 CPU 处于大端模式;
. R00=00001234 R01=00001234:表示成功对堆栈一个 u16 空间写入、读取;
. R03=00000034:表示对堆栈一个 u8 空间成功写入。
同时:
R03=00000034 R04=00001234 R05=00341234
反映出在大端机器
上,程序代码的 u16
和 u8
变量读写后内存空间布局如下:
从以上的测试可以了解到,在 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 访存方式决定的
。
另外,在多字节的赋值中,还应该注意大端字节序的 BE8
和 BE32
不同,更多关于这方面的细节,可参考链接:
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