Chinaunix首页 | 论坛 | 博客
  • 博客访问: 102948
  • 博文数量: 20
  • 博客积分: 496
  • 博客等级: 二等列兵
  • 技术积分: 140
  • 用 户 组: 普通用户
  • 注册时间: 2011-04-07 10:26
文章分类

全部博文(20)

文章存档

2012年(9)

2011年(11)

分类:

2011-06-23 15:03:54

原文地址:GNU C中的嵌入式汇编 作者:billy211

平时在读写代码过程中屡屡会遇到gcc编译环境下的嵌入式汇编问题,觉得有必要总结一下。基于gcc编译器中汇编嵌入C语言的规则以及一些个人体会,现做一下总结,以备以后查阅。在接下来的总结中,我会根据:
1)局部变量的类型(堆栈,寄存器);
2)嵌入式汇编语言中C变量分配寄存器的方法(手工指定、gcc编译器自动指定)
这两种情况的不同组合,并结合elf文件的反汇编内容对比C程序来展开讨论。

约定:下文中凡是提到局部变量之处,指的是变量a和b,凡是提到C变量之处,指的是嵌入式汇编中的C语言变量


情况1:局部变量位于堆栈中,编译器自动绑定C变量到对应的寄存器
所用到的C代码如程序列表1-1所示:

表1-1  情况1下的C语言代码(main.c)
  1. 1 #include "stdio.h"
  2. 2
  3. 3 int main(void)
  4. 4 {
  5. 5 //register int a __asm("eax");
  6. 6 //register int b __asm("ebx");
  7. 7 int a=1;
  8. 8 int b=2;
  9. 9 printf("a=%d b=%d\n", a, b);
  10. 10
  11. 11 __asm volatile("nop");
  12. 12 __asm volatile
  13. 13 ("add %1,%0\n\t"
  14. 14 :"+r"(a)
  15. 15 :"r"(b)
  16. 16 );
  17. 17 __asm volatile("nop");
  18. 18
  19. 19 printf("a=%d b=%d\n", a, b);
  20. 20 return 0;
  21. 21
  22. 22 }
  23. 23

第11行与17行代码中嵌入式了两个nop指令,其主要目的在于快速定位反汇编代码中对应C代码嵌入式汇编部分,方便对比分析,除此之外,别无他途。局部变量a和b位于堆栈中,在嵌入式汇编部分也没有明确为a和b绑定具体的寄存器。将程序列表1-1中的代码编译(gcc -o main main.c),然后运行得到以下结果:
  1. bill@ubuntu:~/test/asm$ ./main
  2. a=1 b=2
  3. a=3 b=2

然后再将可执行程序main反汇编(objdump -S main > main.S),得到如程序列表1-2所示的代码:

表1-2 情况1下main的反汇编代码(main.S)
  1. <main>:
  2. 127 80483c4: push %ebp
  3. 128 80483c5: mov %esp,%ebp
  4. 129 80483c7: and $0xfffffff0,%esp
  5. 130 80483ca: sub $0x20,%esp
  6. 131 80483cd: movl $0x1,0x1c(%esp)
  7. 132 80483d4:
  8. 133 80483d5: movl $0x2,0x18(%esp)
  9. 134 80483dc:
  10. 135 80483dd: mov $0x80484f0,%eax
  11. 136 80483e2: mov 0x18(%esp),%edx
  12. 137 80483e6: mov %edx,0x8(%esp)
  13. 138 80483ea: mov 0x1c(%esp),%edx
  14. 139 80483ee: mov %edx,0x4(%esp)
  15. 140 80483f2: mov %eax,(%esp)
  16. 141 80483f5: call 80482f4 <printf@plt>
  17. 142 80483fa: nop
  18. 143 80483fb: mov 0x18(%esp),%edx
  19. 144 80483ff: mov 0x1c(%esp),%eax
  20. 145 8048403: add %edx,%eax
  21. 146 8048405: mov %eax,0x1c(%esp)
  22. 147 8048409: nop
  23. 148 804840a: mov $0x80484f0,%eax
  24. 149 804840f: mov 0x18(%esp),%edx
  25. 150 8048413: mov %edx,0x8(%esp)
  26. 151 8048417: mov 0x1c(%esp),%edx
  27. 152 804841b: mov %edx,0x4(%esp)
  28. 153 804841f: mov %eax,(%esp)
  29. 154 8048422: call 80482f4 <printf@plt>
  30. 155 8048427: mov $0x0,%eax
  31. 156 804842c: leave
  32. 157 804842d: ret

通过程序列表1-2可以看出,在两个nop之间的代码块即为嵌入式汇编部分。C变量a和b为堆栈变量,在131和133行分别被赋值,然后在嵌入式汇编部分,gcc编译器将a和b值分别赋给寄存器eax和edx,也就是所谓的load操作。然后进行加法操作,eax既是输入寄存器,也是输出寄存器,操作的结果回写内存(堆栈),也就是store操作。在这里,C语言中的嵌入式汇编部分没有明确指出为C变量a和b分配哪个寄存器,gcc编译器自动将其绑定到eax和edx。虽然这里用到了eax和edx,破坏了其原有值,但是从汇编代码可以看出,main函数中,从头到尾没有对这两个寄存器进行push和pop操作,这是为什么呢?!根据ABI(Application Binary Interface)规范,在I386(32位)体系结构下,eax、edx、ecx这三个寄存器是volatile寄存器,可以供callee随便使用而无需为caller保存,因此这里可以放心使用而无需备份原有内容。而对于ebx、edi、esi等寄存器,则属于非volatile寄存器,专属caller 使用,如果callee要使用则需要对其进行push和pop操作以备份和恢复其原有值。
    由于程序列表1-1中只有两个局部变量,只用了eax、edx两个volatile寄存器,因此我们看不出gcc对非volatile寄存器的备份恢复操作,针对这种情况,下面将局部变量数目增加到6个,同样还是进行加法操作,我们来看看会是什么情况。C代码如程序列表1-3所示:

表1-3  局部变量增加为6个的C语言代码(main.c)
                                                                                                                                            
  1.  1 #include "stdio.h"
  2.  2
  3.  3 int main(void)
  4.  4 {
  5.  5 //register int a __asm("eax");
  6.  6 //register int b __asm("ebx");
  7.  7 int a=1;
  8.  8 int b=2;
  9.  9 int c=1;
  10. 10 int d=2;
  11. 11 int e=1;
  12. 12 int f=2;
  13. 13 printf("a=%d b=%d\n", a, b);
  14. 14
  15. 15 __asm volatile("nop");
  16. 16 __asm volatile
  17. 17 ("add %3,%0\n\t"
  18. 18 "add %4,%1\n\t"
  19. 19 "add %5,%2\n\t"
  20. 20 :"+r"(a),"+r"(c),"+r"(e)
  21. 21 :"r"(b),"r"(d),"r"(f)
  22. 22 );
  23. 23 __asm volatile("nop");
  24. 24
  25. 25 printf("a=%d b=%d\n", a, b);
  26. 26 return 0;
  27. 27
  28. 28 }
  29. 29

对程序列表1-3生成的可执行文件main进行反汇编操作,得到如程序列表1-4所示的代码。可以看出,嵌入式汇编部分新增了对寄存器edi、esi、ebx以及ecx的使用,由于前三个寄存器属于非volatile寄存器,因此callee要对其进行维护,也就是main函数要对其进行压栈和弹栈操作。

表1-4 局部变量增加为6个的main反汇编代码
  1. 126 080483c4 <main>:
  2. 127 80483c4: push %ebp
  3. 128 80483c5: mov %esp,%ebp
  4. 129 80483c7: and $0xfffffff0,%esp
  5. 130 80483ca: push %edi
  6. 131 80483cb: push %esi
  7. 132 80483cc: push %ebx
  8. 133 80483cd: sub $0x34,%esp
  9. 134 80483d0: movl $0x1,0x2c(%esp)
  10. 135 80483d7:
  11. 136 80483d8: movl $0x2,0x28(%esp)
  12. 137 ...
  13.  . ...
  14. 152 ...
  15. 153 804841d: nop
  16. 154 804841e: mov 0x28(%esp),%ebx
  17. 155 8048422: mov 0x20(%esp),%esi
  18. 156 8048426: mov 0x18(%esp),%edi
  19. 157 804842a: mov 0x2c(%esp),%ecx
  20. 158 804842e: mov 0x24(%esp),%edx
  21. 159 8048432: mov 0x1c(%esp),%eax
  22. 160 8048436: add %ebx,%ecx
  23. 161 8048438: add %esi,%edx
  24. 162 804843a: add %edi,%eax
  25. 163 804843c: mov %ecx,0x2c(%esp)
  26. 164 8048440: mov %edx,0x24(%esp)
  27. 165 8048444: mov %eax,0x1c(%esp)
  28. 166 8048448: nop
  29. 167 ...
  30.  . ...
  31. 173 ...
  32. 174 8048466: mov $0x0,%eax
  33. 175 804846b: add $0x34,%esp
  34. 176 804846e: pop %ebx
  35. 177 804846f: pop %esi
  36. 178 8048470: pop %edi
  37. 179 8048471: mov %ebp,%esp
  38. 180 8048473: pop %ebp
  39. 181 8048474: ret

以上属于情况1下的实验。下面来实验一下局部变量位于堆栈,嵌入式汇编中手工为C变量绑定寄存器的情况。

情况2:局部变量位于堆栈中,手工绑定C变量到对应的寄存器
对应的C代码如程序列表2-1所示:

表2-1  情况2下的C语言代码(main.c)
  1.  1 #include "stdio.h"
  2.  2
  3.  3 int main(void)
  4.  4 {
  5.  5 //register int a __asm("eax");
  6.  6 //register int b __asm("ebx");
  7.  7 int a=1;
  8.  8 int b=2;
  9.  9 printf("a=%d b=%d\n", a, b);
  10. 10
  11. 11 __asm vlatile("nop");
  12. 12 __asm volatile
  13. 13 ("add %1,%0\n\t"
  14. 14 :"+a"(a)
  15. 15 :"c"(b)
  16. 16 );
  17. 17 __asm volatile("nop");
  18. 18
  19. 19 printf("a=%d b=%d\n", a, b);
  20. 20 return 0;
  21. 21
  22. 22 }
  23. 23
程序里表中,将变量a和b分别绑定到寄存器eax和ecx。将程序列表2-1中的代码反汇编,得到如程序里表2-2所示的代码:
表2-2  情况2下的反汇编代码
  1. 126 080483c4 <main>:
  2. 127 80483c4: push %ebp
  3. 128 80483c5: mov %esp,%ebp
  4. 129 80483c7: and $0xfffffff0,%esp
  5. 130 80483ca: sub $0x20,%esp
  6. 131 80483cd: movl $0x1,0x1c(%esp)
  7. 132 80483d4:
  8. 133 80483d5: movl $0x2,0x18(%esp)
  9. 134 80483dc:
  10. 135 80483dd: mov $0x80484f0,%eax
  11. 136 80483e2: mov 0x18(%esp),%edx
  12. 137 80483e6: mov %edx,0x8(%esp)
  13. 138 80483ea: mov 0x1c(%esp),%edx
  14. 139 80483ee: mov %edx,0x4(%esp)
  15. 140 80483f2: mov %eax,(%esp)
  16. 141 80483f5: call 80482f4 <printf@plt>
  17. 142 80483fa: nop
  18. 143 80483fb: mov 0x18(%esp),%edx
  19. 144 80483ff: mov 0x1c(%esp),%eax
  20. 145 8048403: mov %edx,%ecx
  21. 146 8048405: add %ecx,%eax
  22. 147 8048407: mov %eax,0x1c(%esp)
  23. 148 804840b: nop
  24. 149 804840c: mov $0x80484f0,%eax
  25. 150 8048411: mov 0x18(%esp),%edx
  26. 151 8048415: mov %edx,0x8(%esp)
  27. 152 8048419: mov 0x1c(%esp),%edx
  28. 153 804841d: mov %edx,0x4(%esp)
  29. 154 8048421: mov %eax,(%esp)
  30. 155 8048424: call 80482f4 <printf@plt>
  31. 156 8048429: mov $0x0,%eax
  32. 157 804842e: leave
  33. 158 804842f: ret
可以看出,在程序列表2-2中,在具体计算中,变量a和b的值确实赋给了寄存器eax和ecx,对于变量b来说,从堆栈取出的过程中,其值先赋给了寄存器edx,然后再由x赋给寄存器ecx,看来是“多此一举”了,其实我感觉这是gcc按着固定的套路来编译C代码的结果,在对堆栈里面的局部变量进行操作之前,gcc先不管下面如何操作,固定的将堆栈里面的变量用寄存器序列eax,edx,ecx 取出来,至于后面如何使用这些变量,以及将这些变量赋值给谁,那是后话,因此这里稍显机械,存在优化空间。对于变量的回写过程,同样也是机械的通过寄存器序列eax,edx,ecx来实施。在嵌入式汇编部分,如果将变量b绑定edx,则不存在这里的"多此一举"情况,节省一条mov操作指令。同样,如果为变量b绑定非volatile寄存器的话,例如ebx,除了无法节省这条mov操作指令之外,反而还会增加一条push和一条pop操作指令,用于维护ebx的内容。
说倒取数(load)以及回写(store),这里不得不多说两句,在取数的过程中,也就是load过程,gcc选择使用寄存器序列的顺序是eax,edx,ecx,...,也就是说如果有1个变量要load的话,就用eax,同样,如果有两个变load操作,就用eax和edx,依次类推,如果有三个load操作,就用eax、edx、ecx。这时候volatile寄存器序列已经用完,如果有4个load操作怎么办,显然是用非volatile寄存器了,这时候在使用之前就要实施push操作,使用完之后要实施pop操作。同理,对于回写操作,也就是store操作,按着同样规则使用相同的寄存器序列。在程序列表2-1中,虽然变量b绑定的是ecx,但是这里仍然按着load两次参数的"死板"规则来取数,先使用寄存器edx取b,再使用寄存器eax取a值,然后再将寄存器edx的值赋给寄存器ecx,即手工绑定的寄存器。对于回写过程,规则依然死板,如果为变量a绑定寄存器ecx的话,也就是说ecx要回写,这时候gcc的规则现将ecx的值赋给eax,然后再将eax的值回写堆栈。为什么要用eax,前面已经说过,因为这里只有一次store操作,所以别无他选,老老实实用eax回写吧。总结一下,load过程使用寄存器序列的规则如下:
1)如果进行1次load操作,使用寄存器eax: “mov 0x(xx)%esp, eax”
2)如果进行2次load操作,使用寄存器edx,eax: “mov 0x(xx)%esp, edx” “mov 0x(xx)%esp, eax”
3)如果进行3次load操作,使用寄存器ecx,edx,eax: “mov 0x(xx)%esp, ecx” “mov 0x(xx)%esp, edx” “mov 0x(xx)%esp, eax”
4)如果进行4次load操作,使用寄存器ebx,ecx,edx,eax:“mov 0x(xx)%esp, ebx” “mov 0x(xx)%esp, ecx” “mov 0x(xx)%esp, edx” “mov 0x(xx)%esp, eax”
5)以下依次类推,除了以上寄存器序列,还会使用edi,esi等寄存器,如果寄存器依然不够使用,那么可能会就循环使用以上寄存器,具体情况有待验证。

在store过程中,寄存器的使用规则如下:
1)如果进行1次store操作,使用寄存器eax: “mov eax, 0x(xx)%esp,”
2)如果进行2次store操作,使用寄存器edx,eax: “mov edx, 0x(xx)%esp” “mov eax, 0x(xx)%esp”
3)如果进行3次store操作,使用寄存器ecx,edx,eax: “mov ecx, 0x(xx)%esp” “mov edx, 0x(xx)%esp” “mov eax, 0x(xx)%esp”
4)如果进行4次store操作,使用寄存器ebx,ecx,edx,eax:“mov ebx, 0x(xx)%esp” “mov ecx, 0x(xx)%esp” “mov edx, 0x(xx)%esp” “mov eax, 0x(xx)%esp”
5)以下依次类推,出了以上寄存器序列,还会使用edi,esi等寄存器,如果寄存器依然不够使用,那么可能会就循环使用以上寄存器,具体情况有待验证。
通过以上情况不难发现,在使用堆栈局部变量的情况下,对于嵌入式汇编里面的寄存其绑定问题,还是交由gcc启动完成比较好,可以获得较好的程序性能,免得额外指令的浪费,而且频繁的、不必要的访存操作是不被鼓励的(push,pop)。既然gcc的load和store操作有着自己的规则,那么我们就少些不必要的干预,让gcc获得更大的自主空间,按着自己的规则进行优化。如果实在要干预的话,理解以上规则对于程序的优化是有好处的。当然,这些是我自己目前的初步总结,更加详细准确的规则也许gcc mannual里面已经有了,不过我自己总结得来的认识更加深刻,虽然有待完善。


情况3:局部变量位于寄存器中,自动绑定C变量到寄存器
C语言代码如程序列表3-1所示:

表3-1 情况3下的C语言代码:
  1.  1 #include "stdio.h"
  2.  2
  3.  3 int main(void)
  4.  4 {
  5.  5 register int a __asm("eax")=1;
  6.  6 register int b __asm("ebx")=2;
  7.  7 printf("a=%d b=%d\n", a, b);
  8. 10 __asm volatile
  9. 11 ("add %1,%0\n\t"
  10. 12 :"+r"(a)
  11. 13 :"r"(b)
  12. 14 );
  13. 15 __asm volatile("nop");
  14. 16
  15. 17 printf("a=%d b=%d\n", a, b);
  16. 18 return 0;
  17. 19
  18. 20 }
  19. 21
这里将c变量a和b分别位于寄存器eax和ebx中,C代码中为这两个变量赋值。在嵌入式汇编部分,让gcc自动为这两个变量绑定寄存器。将程序列表3-1中的代码编译后执行,得到如下所示结果:
  1. bill@ubuntu:~/test/asm$ ./main
  2. a=1 b=2
  3. a=10 b=2
非常抱歉,这个结果比较意外,程序列表3-1中明明执行的是a+b操作,结果存放于a中,按道理来说,a应该等于3才是,正如情况一的执行结果那样。可是这里偏偏却不是这样子。为什么???先对main程序进行反汇编,我们来一看究竟,反汇编结果如程序列表3-2所示:
表3-2 情况3下的反汇编结果:
  1. 126 080483c4 <main>:
  2. 127 80483c4: push %ebp
  3. 128 80483c5: mov %esp,%ebp
  4. 129 80483c7: and $0xfffffff0,%esp
  5. 130 80483ca: push %ebx
  6. 131 80483cb: sub $0x1c,%esp
  7. 132 80483ce: mov $0x1,%eax
  8. 133 80483d3: mov $0x2,%ebx
  9. 134 80483d8: mov %ebx,%ecx
  10. 135 80483da: mov $0x80484e0,%edx
  11. 136 80483df: mov %ecx,0x8(%esp)
  12. 137 80483e3: mov %eax,0x4(%esp)
  13. 138 80483e7: mov %edx,(%esp)
  14. 139 80483ea: call 80482f4 <printf@plt>
  15. 140 80483ef: nop
  16. 141 80483f0: add %ebx,%eax
  17. 142 80483f2: nop
  18. 143 80483f3: mov %ebx,%edx
  19. 144 80483f5: mov $0x80484e0,%ecx
  20. 145 80483fa: mov %edx,0x8(%esp)
  21. 146 80483fe: mov %eax,0x4(%esp)
  22. 147 8048402: mov %ecx,(%esp)
  23. 148 8048405: call 80482f4 <printf@plt>
  24. 149 804840a: mov $0x0,%eax
  25. 150 804840f: add $0x1c,%esp
  26. 151 8048412: pop %ebx
  27. 152 8048413: mov %ebp,%esp
  28. 153 8048415: pop %ebp
  29. 154 8048416: ret
从程序列表3-2中,我们明显可以看出,局部变量的绑定成功了,a绑定eax,b绑定ecx,两个nop指令之间的计算指令也是对的,add %ebx,%eax,结果保存于eax,由于在嵌入式汇编部分的绑定采用的是自动方式,因此gcc编译将计就计,保持原有寄存器绑定不变,可是打印出来的结果却偏偏不正确。在为a、b赋值和执行二者之间的加法操作的中间还有变故吗?有!确定有,仔细看,我们在执行加法指令之前,曾经调用了一次printf函数,总所周知,eax充当默认的函数返回值寄存器,也就是在调用了函数printf之后,寄存器eax原有的值(1)已经被破坏掉了,用作存放函数printf的返回值了,所以当程序真正执行add指令时,eax的值已经不再是1,因此得到的结果也不会是3,所以就有了刚才的错误执行结果。为此,我们在c代码中稍作改动,为了维持eax原有的值,我们在第一次执行完printf操作之后,对a再进行依次赋值操作。如程序列表3-3所示:
表3-3 修正后的C代码
  1.   1 #include "stdio.h"
  2.   2
  3.   3 int main(void)
  4.   4 {
  5.   5 register int a __asm("eax")=1;
  6.   6 register int b __asm("ebx")=2;
  7.   7
  8.   8 printf("a=%d b=%d\n", a, b);
  9.   9 a = 1;
  10.  10
  11.  11 __asm volatile("nop");
  12.  12 __asm volatile
  13.  13 ("addl %1,%0\n\t"
  14.  14 :"+r"(a)
  15.  15 :"r"(b)
  16.  16 );
  17.  17 __asm volatile("nop");
  18.  18
  19.  19 printf("a=%d b=%d\n", a, b);
  20.  20 return 0;
  21.  21
  22.  22 }
  23.  23
编译后得到的执行结果如下:
  1. bill@ubuntu:~/test/asm$ ./main
  2. a=1 b=2
  3. a=3 b=2
结果正确,恢复了正常,我们在对其进行反汇编,得到的代码如列表3-4所示:
表3-4 修正后的反汇编代码
  1. 126 080483c4 <main>:
  2. 127 80483c4: push %ebp
  3. 128 80483c5: mov %esp,%ebp
  4. 129 80483c7: and $0xfffffff0,%esp
  5. 130 80483ca: push %ebx
  6. 131 80483cb: sub $0x1c,%esp
  7. 132 80483ce: mov $0x1,%eax
  8. 133 80483d3: mov $0x2,%ebx
  9. 134 80483d8: mov %ebx,%ecx
  10. 135 80483da: mov $0x80484e0,%edx
  11. 136 80483df: mov %ecx,0x8(%esp)
  12. 137 80483e3: mov %eax,0x4(%esp)
  13. 138 80483e7: mov %edx,(%esp)
  14. 139 80483ea: call 80482f4 <printf@plt>
  15. 140 80483ef: mov $0x1,%eax
  16. 141 80483f4: nop
  17. 142 80483f5: add %ebx,%eax
  18. 143 80483f7: nop
  19. 144 80483f8: mov %ebx,%edx
  20. 145 80483fa: mov $0x80484e0,%ecx
  21. 146 80483ff: mov %edx,0x8(%esp)
  22. 147 8048403: mov %eax,0x4(%esp)
  23. 148 8048407: mov %ecx,(%esp)
  24. 149 804840a: call 80482f4 <printf@plt>
  25. 150 804840f: mov $0x0,%eax
  26. 151 8048414: add $0x1c,%esp
  27. 152 8048417: pop %ebx
  28. 153 8048418: mov %ebp,%esp
  29. 154 804841a: pop %ebp
  30. 155 804841b: ret
由程序列表3-4可以见,在第一次调用printf函数之后由对a绑定的寄存器重新赋值,使得add执行结果正确。说道这里,就不得不再提提关于volatile寄存器的问题。前面说过,eax,edx,ecx属于volatile寄存器,callee可以随意使用而无需维护其内容,那么如果这三个寄存器里面有值需要维护怎么办呢,显然由caller负责维护,对于main函数本省来讲,它属于callee,可以随意自由使用这三个volatile寄存器,但是相对printf函数来说,main属于caller,如过eax,edx,ecx有需要维护的值,那么这个维护工作必须交由caller,也就是main来完成。而程序列表3-1中代码并没有维护eax寄存器的值,因此产生错误。需要注意的是,对于非volatile寄存器ebx,gcc则自动添加了维护代码(push、pop)。在实验过程中还发现,如果为局部变量绑定寄存器edx,ecx,结果均为出现错误。而相反,如果为局部变量绑定寄存器edi,esi,ebx则不会出现错误,纠其原因还在于gcc会自动维护edi,esi,ebx这三个非volatile的值,而且在做函数调用的时候,也不会轻易使用这三个非volatile寄存器进行store操作--往堆栈传递参数,而是更多的使用eax,edx,ecx,除非进行store操作的次数比较多,才会使用非volatile寄存器edi,esi,ebx等,在使用之前当然要push,另外,如果局部变量绑定了非volatile寄存器中,那么在store的过程中就会避免使用这些已经被局部变量绑定的寄存器,即使store过程中寄存器不够用,而对于volatile寄存器则大不同,既是前面已经绑定了局部变量,但是在做store操作的时候照用不误,因为他们是sotre和load操作的首选寄存器。这些寄存器是volatile属性,“不靠谱”,需要多加小心使用,自己注意维护。因此对于volatile(eax,edx,ecx)和非volatile(edi,esi,ebx)寄存器总结如下:
1) volatile寄存器是进行load和store操作的首先寄存器,也是函数调用的返回值寄存器,如果前面将这些寄存器绑定了某个局部变量,那这个变量在进行load或者store的过程中,会被破坏掉,在函数调用过程中,也会被破坏掉,因此需要注意维护。
2)非volatile寄存器只有在volatile寄存器不够用的情况下,才在load、store操作中派出用场,当然,还是由gcc自动维护其值(push,pop)。如果前面将这些寄存器绑定了帮个变量,那么在load和store的过程中不会再有已经绑定局部变量的寄存器出现。
总之,对于非volatile寄存器的使用,采取的是比较“客气”的态度,能不用则尽量不用,gcc会在函数的开头和结尾进行push和pop操作,维护其原有内容,在使用的过程中不会破坏其中的既定值。而对voaltile寄存器的使用,则要粗暴的多,而且会尽量频繁的使用,gcc不会在函数的开头和结尾进行push和pop操作,也不会在使用过程中顾及其值是否被破坏。

情况4 :局部变量位于寄存器中,手工绑定C变量到寄存器
这里将局部变量绑定某一寄存器,而嵌入式汇编部分将对应C变量手工绑定到某个寄存器。代码如程序列表4-1所示:
表4-1 情况4下的C代码:
  1.   1 #include "stdio.h"
  2.   2
  3.   3 int main(void)
  4.   4 {
  5.   5 register int a __asm("edi")=1;
  6.   6 register int b __asm("esi")=2;
  7.   7 printf("a=%d b=%d\n", a, b);
  8.   8
  9.   9 //a = 1;
  10.  10 __asm volatile("nop");
  11.  11 __asm volatile
  12.  12 ("addl %1,%0\n\t"
  13.  13 :"+a"(a)
  14.  14 :"b"(b)
  15.  15 );
  16.  16 __asm volatile("nop");
  17.  17
  18.  18 printf("a=%d b=%d\n", a, b);
  19.  19 return 0;
  20.  20
  21.  21 }
  22.  22
鉴于情况3的结果,这里将局部变量绑定非volatile寄存器,我们将焦点集中于嵌入式汇编中手工绑定C变量到寄存器的情况。程序编译运行结果正确,如情况1一样结果。通过实验发现,在程序列表4-1中的地13、14行,无论如何制定寄存器,其结构均正确,因为两个nop之间的嵌入式汇编不会再出现其他代码使用voaltile寄存器的情况,因此寄存器值的一致性可以得到保证。如果采用自动绑定形式,gcc会维持原有的绑定,如果手工绑定到具体寄存器,gcc也会照搬,只不过这样会增加一些指令,用于在先前绑定(局部变量绑定)寄存器和后来绑定寄存器(嵌入式汇编中C变量绑定)之间传递变量值。程序列表4-1中的代码反汇编结果如表4-2所示:
  1. 126 080483c4 <main>:
  2. 127 80483c4: push %ebp
  3. 128 80483c5: mov %esp,%ebp
  4. 129 80483c7: and $0xfffffff0,%esp
  5. 130 80483ca: push %edi
  6. 131 80483cb: push %esi
  7. 132 80483cc: push %ebx
  8. 133 80483cd: sub $0x14,%esp
  9. 134 80483d0: mov $0x1,%edi
  10. 135 80483d5: mov $0x2,%esi
  11. 136 80483da: mov %esi,%ecx
  12. 137 80483dc: mov %edi,%edx
  13. 138 80483de: mov $0x80484f0,%eax
  14. 139 80483e3: mov %ecx,0x8(%esp)
  15. 140 80483e7: mov %edx,0x4(%esp)
  16. 141 80483eb: mov %eax,(%esp)
  17. 142 80483ee: call 80482f4 <printf@plt>
  18. 143 80483f3: nop
  19. 144 80483f4: mov %edi,%eax
  20. 145 80483f6: mov %esi,%ebx
  21. 146 80483f8: add %ebx,%eax
  22. 147 80483fa: mov %eax,%edi
  23. 148 80483fc: nop
  24. 149 80483fd: mov %esi,%edx
  25. 150 80483ff: mov %edi,%eax
  26. 151 8048401: mov $0x80484f0,%ecx
  27. 152 8048406: mov %edx,0x8(%esp)
  28. 153 804840a: mov %eax,0x4(%esp)
  29. 154 804840e: mov %ecx,(%esp)
  30. 155 8048411: call 80482f4 <printf@plt>
  31. 156 8048416: mov $0x0,%eax
  32. 157 804841b: add $0x14,%esp
  33. 158 804841e: pop %ebx
  34. 159 804841f: pop %esi
  35. 160 8048420: pop %edi
  36. 161 8048421: mov %ebp,%esp
  37. 162 8048423: pop %ebp
  38. 163 8048424: ret
由程序列表4-2可以看出,虽然在嵌入式汇编中重新对变量进行了寄存器绑定,但是最终的计算结构还是要传回先前绑定的寄存器中edi。

至此,针对本文中示例代码的嵌入式汇编讨论告一段落。这里主要针对voaltile和非volatile两大类寄存器集在具体编译过程中的使用规则而展开讨论。由于讨论是限定在具体的程序代码之中,难以顾及方方面面,因此会存在这样或者那样的局限性,仅仅是个人的一些浅显总结,今后随着实验的深入而对本文进行扩充。如有错误之处,还请指导交流。


阅读(1365) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~