uboot代码中有这么一句话“#define DECLARE_GLOBAL_DATA_PTR register volatile gd_t *gd asm ("r8")”,困扰了山人多时。经过多番求索,才得知原来是定义了一个全局的寄存器变量gd_t(r8是它的专用寄存器)。
详细解释一下,register意思是定义的变量保存在寄存器中,volatile代表禁止编译器优化,后边的asm ("r8")说的是使用的寄存器是r8。
一、何谓寄存器变量
百度百科
在程序运行时,根据需要到内存中相应的存储单元中调用,如果一个变量在程序中频繁使用,例如循环变量,那么,系统就必须多次访问内存中的该单元,影响程序的执行效率。因此,C\C++语言还定义了一种变量,不是保存在内存上,而是直接存储在CPU中的寄存器中,这种变量称为寄存器变量。
山人认为寄存器变量的优势的确是提高了变量的访问速度,也牺牲了宝贵的寄存器资源。在学谭浩强的C语言时,书中有关寄存器变量的介绍。
二、APCS 简介
本文讲的是寄存器变量的事,看似与APCS无关。但是,实质上APCS涉及了寄存器是怎样被使用的,也就关寄存器变量的事了。所以,还是需要先介绍APCS。以下文字摘录于“arm指令集”。
APCS,ARM 过程调用标准(ARM Procedure Call Standard),提供了紧凑的编写例程的一种机制,定义的例程可以与其他例程交织在一起。最显著的一点是对这些例程来自哪里没有明确的限制。它们可以编译自 C、 Pascal、也可以是用汇编语言写成的。
APCS 定义了:
- 对寄存器使用的限制。
- 使用栈的惯例。
- 在函数调用之间传递/返回参数。
- 可以被‘回溯’的基于栈的结构的格式,用来提供从失败点到程序入口的函数(和给予的参数)的列表。
APCS 不一个单一的给定标准,而是一系列类似但在特定条件下有所区别的标准。例如,APCS-R (用于 RISC OS)规定在函数进入时设置的标志必须在函数退出时复位。在 32 位标准下,并不是总能知道进入标志的(没有 USR_CPSR),所以你不需要恢复它们。如你所预料的那样,在不同版本间没有相容性。希望恢复标志的代码在它们未被恢复的时候可能会表现失常...
如果你开发一个基于 ARM 的系统,不要求你去实现 APCS。但建议你实现它,因为它不难实现,且可以使你获得各种利益。但是,如果要写用来与编译后的 C 连接的汇编代码,则必须使用 APCS。编译器期望特定的条件,在你的加入(add-in)代码中必须得到满足。一个好例子是 APCS 定义 a1 到 a4 可以被破坏,而 v1 到 v6 必须被保护。现在我确信你正在挠头并自言自语“a 是什么? v 是什么?”。所以首先介绍 APCS-R 寄存器定义...
ARM寄存器
Reg# | APCS | 意义 |
R0 | a1 | 工作寄存器 |
R1 | a2 | ·· |
R2 | a3 | ·· |
R3 | a4 | ·· |
R4 | v1 | 必须保护 |
R5 | v2 | ·· |
R6 | v3 | ·· |
R7 | v4 | ·· |
R8 | v5 | ·· |
R9 | v6 | ·· |
R10 | sl | 栈限制 |
R11 | fp | 帧指针 |
R12 | ip | |
R13 | sp | 栈指针 |
R14 | lr | 连接寄存器 |
R15 | pc | 程序计数器 |
笔者实验总结
通过反汇编查看C语言的代码,可以知道r0-r3用作形参传递参数,倘若形参超过4个,则需要用堆栈来传递其余的参数。r0-r3在函数中间是作为中间变量的,在退出函数之后,其值也不用特别处理(试想形参也没有返回值,而中间变量是暂存数据,也不需要先保存再弹出)。由此,下结论r0-r3是可以被破坏的。
r4-r14更像是全局变量,因为假设在函数中因为中间变量不够用而使用了r4-r12中的一个或者某几个,再进入函数时将用到的寄存器保存,出去的时候再弹出,所以r4-r12在经过函数之后是保持不变的。
三、如何在程序中实现定义寄存器变量
以实验来说明定义寄存器变量的方法
head.s
1 .text 2 .global _start 3 _start: 4 ldr sp, =4096 5 bl init 6 bl main 7 halt_loop: 8 b halt_loop
init.c
1 register volatile int* gd_t asm("r8"); 2 void init() 3 { 4 gd_t = (int *)0; 5 }
main.c
1 register volatile int* gd_t asm ("r8"); 2 3 int main(void) 4 { 5 gd_t = (int* )1; 6 while(1); 7 return 0; 8 }
Makefile
objs := head.o init.o main.o test.bin : $(objs) arm-linux-ld -Ttext 0x0 -o test_elf $^ arm-linux-objcopy -O binary -S test_elf $@ arm-linux-objdump -D -m arm test_elf > test.dis %.o:%.c arm-linux-gcc -Wall -c -O2 -o $@ $< %.o:%.S arm-linux-gcc -Wall -c -O2 -o $@ $< clean: rm -f test.dis test.bin test_elf *.o
查看反汇编内容
test.dis
1 test_elf: file format elf32-littlearm 2 3 Disassembly of section .text: 4 5 00000000 <_start>: 6 0: e3a0da01 mov sp, #4096 ; 0x1000 7 4: eb000001 bl 108 8: eb000002 bl 18 9 10 0000000c : 11 c: eafffffe b c 12 13 00000010 : 14 10: e3a09000 mov r8, #0 ; 0x0 15 14: e1a0f00e mov pc, lr 16 17 00000018 : 18 18: e3a08001 mov r8, #1 ; 0x1 19 1c: eafffffe b 1c 0x4> 20 Disassembly of section .comment: 21 22 00000000 <.comment>: 23 0: 43434700 cmpmi r3, #0 ; 0x0 24 4: 4728203a undefined 25 8: 2029554e eorcs r5, r9, lr, asr #10 26 c: 2e342e33 mrccs 14, 1, r2, cr4, cr3, {1} 27 10: 47000035 smladxmi r0, r5, r0, r0 28 14: 203a4343 eorcss r4, sl, r3, asr #6 29 18: 554e4728 strplb r4, [lr, #-1832] 30 1c: 2e332029 cdpcs 0, 3, cr2, cr3, cr9, {1} 31 20: 00352e34 eoreqs r2, r5, r4, lsr lr
寄存器全局变量与普通的全局变量使用区别
普通的全局变量使用是在一个文件中定义全局变量,然后通过“extern”将其作用域扩展到其他的文件;寄存器的全局变量是每一个使用这个全局变量的文件都要对这个全局变量定义。
为什么有这样的区别呢
普通全局变量在一个文件被定义后,就会在相应的数据段(data或者bss)开辟一个空间,这个空间编译器会给它命一个名字。例如“int a=10;”就会在data段开辟一个4字节的空间,并且编译器给这个空间命名为a。其它文件也需要调用这个全局变量时,是对这个变量进行声明,例如“extern int a;”。在连接程序阶段,连接器会把所有使用a变量的代码都定位在之前定义时在data段开辟命名为a的空间。
寄存器全局变量,它的存储空间已经确定。当对寄存器全局变量的定义进行编译处理时,不会试图从data或者bss开辟空间,而是直接使用寄存器(例如实验程序中,直接使用r8来处理gd_t)。因为不存在编译器产生连接时所需要有关全局变量的标号(编译器不会产生gd_t的标号),所以实际上也不存在“哪一个文件是定义,哪一个文件是声明的问题了”。
当init.c文件使用了寄存器全局变量gd_t,它在编译时被r8替换。当main.c也使用了gd_t,它也被r8替换。实际上,在连接过程中已经没有处理gd_t的任务。这与普通全局变量的编译、连接是有本质的区别。
为了体现这种巨大差异,再做一个实验修改init.c的内容,其他保持不变。
init.c
1 register volatile int* gd_t asm("r9"); 2 void init() 3 { 4 gd_t = (int *)0; 5 }
对应的反汇编代码
test.dis
1 test_elf: file format elf32-littlearm 2 3 Disassembly of section .text: 4 5 00000000 <_start>: 6 0: e3a0da01 mov sp, #4096 ; 0x1000 7 4: eb000001 bl 108 8: eb000002 bl 18 9 10 0000000c : 11 c: eafffffe b c 12 13 00000010 : 14 10: e3a09000 mov r9, #0 ; 0x0 15 14: e1a0f00e mov pc, lr 16 17 00000018 : 18 18: e3a08001 mov r8, #1 ; 0x1 19 1c: eafffffe b 1c 0x4> 20 Disassembly of section .comment: 21 22 00000000 <.comment>: 23 0: 43434700 cmpmi r3, #0 ; 0x0 24 4: 4728203a undefined 25 8: 2029554e eorcs r5, r9, lr, asr #10 26 c: 2e342e33 mrccs 14, 1, r2, cr4, cr3, {1} 27 10: 47000035 smladxmi r0, r5, r0, r0 28 14: 203a4343 eorcss r4, sl, r3, asr #6 29 18: 554e4728 strplb r4, [lr, #-1832] 30 1c: 2e332029 cdpcs 0, 3, cr2, cr3, cr9, {1} 31 20: 00352e34 eoreqs r2, r5, r4, lsr lr
可以看到编译能够正常通过,也就是说gd_t在不同的文件中可以使用不同的寄存器,因为在连接过程中没有gd_t处理的任务(它们都被寄存器所代替)。
四、使用全局寄存器变量需要注意的地方
1、每一个使用这个全局变量的文件都要对这个全局变量定义,而且要一样
倘若像修改后的代码一样,同一个全局变量gd_t却在不同的文件中使用了不同的寄存器,结果不能实现全局变量的效果。
2、确保汇编程序中没有随意修改所使用寄存器的值
寄存器全局变量有可能会在汇编程序中被破坏,可能因为寄存器不够用或者中断服务。要做的任务就是,使用前对其保存和退出使用后对其恢复。
3、不能使用r0-r3作为寄存器全局变量
因为它们遵从APCS规则,会被经常破坏,不能当做寄存器全局变量来用。而r4-r12的特性适合全局变量来使用。
后记
本文为笔者实验以及查找资料所得结论,可能有谬误,望读者发现指正。