平时在读写代码过程中屡屡会遇到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 #include "stdio.h"
- 2
- 3 int main(void)
- 4 {
- 5 //register int a __asm("eax");
- 6 //register int b __asm("ebx");
- 7 int a=1;
- 8 int b=2;
- 9 printf("a=%d b=%d\n", a, b);
- 10
- 11 __asm volatile("nop");
- 12 __asm volatile
- 13 ("add %1,%0\n\t"
- 14 :"+r"(a)
- 15 :"r"(b)
- 16 );
- 17 __asm volatile("nop");
- 18
- 19 printf("a=%d b=%d\n", a, b);
- 20 return 0;
- 21
- 22 }
- 23
第11行与17行代码中嵌入式了两个nop指令,其主要目的在于快速定位反汇编代码中对应C代码嵌入式汇编部分,方便对比分析,除此之外,别无他途。局部变量a和b位于堆栈中,在嵌入式汇编部分也没有明确为a和b绑定具体的寄存器。将程序列表1-1中的代码编译(gcc -o main main.c),然后运行得到以下结果:
- bill@ubuntu:~/test/asm$ ./main
- a=1 b=2
- a=3 b=2
然后再将可执行程序main反汇编(objdump -S main > main.S),得到如程序列表1-2所示的代码:
表1-2 情况1下main的反汇编代码(main.S)
- <main>:
- 127 80483c4: push %ebp
- 128 80483c5: mov %esp,%ebp
- 129 80483c7: and $0xfffffff0,%esp
- 130 80483ca: sub $0x20,%esp
- 131 80483cd: movl $0x1,0x1c(%esp)
- 132 80483d4:
- 133 80483d5: movl $0x2,0x18(%esp)
- 134 80483dc:
- 135 80483dd: mov $0x80484f0,%eax
- 136 80483e2: mov 0x18(%esp),%edx
- 137 80483e6: mov %edx,0x8(%esp)
- 138 80483ea: mov 0x1c(%esp),%edx
- 139 80483ee: mov %edx,0x4(%esp)
- 140 80483f2: mov %eax,(%esp)
- 141 80483f5: call 80482f4 <printf@plt>
- 142 80483fa: nop
- 143 80483fb: mov 0x18(%esp),%edx
- 144 80483ff: mov 0x1c(%esp),%eax
- 145 8048403: add %edx,%eax
- 146 8048405: mov %eax,0x1c(%esp)
- 147 8048409: nop
- 148 804840a: mov $0x80484f0,%eax
- 149 804840f: mov 0x18(%esp),%edx
- 150 8048413: mov %edx,0x8(%esp)
- 151 8048417: mov 0x1c(%esp),%edx
- 152 804841b: mov %edx,0x4(%esp)
- 153 804841f: mov %eax,(%esp)
- 154 8048422: call 80482f4 <printf@plt>
- 155 8048427: mov $0x0,%eax
- 156 804842c: leave
- 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 #include "stdio.h"
- 2
- 3 int main(void)
- 4 {
- 5 //register int a __asm("eax");
- 6 //register int b __asm("ebx");
- 7 int a=1;
- 8 int b=2;
- 9 int c=1;
- 10 int d=2;
- 11 int e=1;
- 12 int f=2;
- 13 printf("a=%d b=%d\n", a, b);
- 14
- 15 __asm volatile("nop");
- 16 __asm volatile
- 17 ("add %3,%0\n\t"
- 18 "add %4,%1\n\t"
- 19 "add %5,%2\n\t"
- 20 :"+r"(a),"+r"(c),"+r"(e)
- 21 :"r"(b),"r"(d),"r"(f)
- 22 );
- 23 __asm volatile("nop");
- 24
- 25 printf("a=%d b=%d\n", a, b);
- 26 return 0;
- 27
- 28 }
- 29
对程序列表1-3生成的可执行文件main进行反汇编操作,得到如程序列表1-4所示的代码。可以看出,嵌入式汇编部分新增了对寄存器edi、esi、ebx以及ecx的使用,由于前三个寄存器属于非volatile寄存器,因此callee要对其进行维护,也就是main函数要对其进行压栈和弹栈操作。
表1-4 局部变量增加为6个的main反汇编代码
- 126 080483c4 <main>:
- 127 80483c4: push %ebp
- 128 80483c5: mov %esp,%ebp
- 129 80483c7: and $0xfffffff0,%esp
- 130 80483ca: push %edi
- 131 80483cb: push %esi
- 132 80483cc: push %ebx
- 133 80483cd: sub $0x34,%esp
- 134 80483d0: movl $0x1,0x2c(%esp)
- 135 80483d7:
- 136 80483d8: movl $0x2,0x28(%esp)
- 137 ...
- . ...
- 152 ...
- 153 804841d: nop
- 154 804841e: mov 0x28(%esp),%ebx
- 155 8048422: mov 0x20(%esp),%esi
- 156 8048426: mov 0x18(%esp),%edi
- 157 804842a: mov 0x2c(%esp),%ecx
- 158 804842e: mov 0x24(%esp),%edx
- 159 8048432: mov 0x1c(%esp),%eax
- 160 8048436: add %ebx,%ecx
- 161 8048438: add %esi,%edx
- 162 804843a: add %edi,%eax
- 163 804843c: mov %ecx,0x2c(%esp)
- 164 8048440: mov %edx,0x24(%esp)
- 165 8048444: mov %eax,0x1c(%esp)
- 166 8048448: nop
- 167 ...
- . ...
- 173 ...
- 174 8048466: mov $0x0,%eax
- 175 804846b: add $0x34,%esp
- 176 804846e: pop %ebx
- 177 804846f: pop %esi
- 178 8048470: pop %edi
- 179 8048471: mov %ebp,%esp
- 180 8048473: pop %ebp
- 181 8048474: ret
以上属于情况1下的实验。下面来实验一下局部变量位于堆栈,嵌入式汇编中手工为C变量绑定寄存器的情况。
情况2:局部变量位于堆栈中,手工绑定C变量到对应的寄存器
对应的C代码如程序列表2-1所示:
表2-1 情况2下的C语言代码(main.c)
- 1 #include "stdio.h"
- 2
- 3 int main(void)
- 4 {
- 5 //register int a __asm("eax");
- 6 //register int b __asm("ebx");
- 7 int a=1;
- 8 int b=2;
- 9 printf("a=%d b=%d\n", a, b);
- 10
- 11 __asm vlatile("nop");
- 12 __asm volatile
- 13 ("add %1,%0\n\t"
- 14 :"+a"(a)
- 15 :"c"(b)
- 16 );
- 17 __asm volatile("nop");
- 18
- 19 printf("a=%d b=%d\n", a, b);
- 20 return 0;
- 21
- 22 }
- 23
程序里表中,将变量a和b分别绑定到寄存器eax和ecx。将程序列表2-1中的代码反汇编,得到如程序里表2-2所示的代码:
表2-2 情况2下的反汇编代码
- 126 080483c4 <main>:
- 127 80483c4: push %ebp
- 128 80483c5: mov %esp,%ebp
- 129 80483c7: and $0xfffffff0,%esp
- 130 80483ca: sub $0x20,%esp
- 131 80483cd: movl $0x1,0x1c(%esp)
- 132 80483d4:
- 133 80483d5: movl $0x2,0x18(%esp)
- 134 80483dc:
- 135 80483dd: mov $0x80484f0,%eax
- 136 80483e2: mov 0x18(%esp),%edx
- 137 80483e6: mov %edx,0x8(%esp)
- 138 80483ea: mov 0x1c(%esp),%edx
- 139 80483ee: mov %edx,0x4(%esp)
- 140 80483f2: mov %eax,(%esp)
- 141 80483f5: call 80482f4 <printf@plt>
- 142 80483fa: nop
- 143 80483fb: mov 0x18(%esp),%edx
- 144 80483ff: mov 0x1c(%esp),%eax
- 145 8048403: mov %edx,%ecx
- 146 8048405: add %ecx,%eax
- 147 8048407: mov %eax,0x1c(%esp)
- 148 804840b: nop
- 149 804840c: mov $0x80484f0,%eax
- 150 8048411: mov 0x18(%esp),%edx
- 151 8048415: mov %edx,0x8(%esp)
- 152 8048419: mov 0x1c(%esp),%edx
- 153 804841d: mov %edx,0x4(%esp)
- 154 8048421: mov %eax,(%esp)
- 155 8048424: call 80482f4 <printf@plt>
- 156 8048429: mov $0x0,%eax
- 157 804842e: leave
- 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 #include "stdio.h"
- 2
- 3 int main(void)
- 4 {
- 5 register int a __asm("eax")=1;
- 6 register int b __asm("ebx")=2;
- 7 printf("a=%d b=%d\n", a, b);
- 10 __asm volatile
- 11 ("add %1,%0\n\t"
- 12 :"+r"(a)
- 13 :"r"(b)
- 14 );
- 15 __asm volatile("nop");
- 16
- 17 printf("a=%d b=%d\n", a, b);
- 18 return 0;
- 19
- 20 }
- 21
这里将c变量a和b分别位于寄存器eax和ebx中,C代码中为这两个变量赋值。在嵌入式汇编部分,让gcc自动为这两个变量绑定寄存器。将程序列表3-1中的代码编译后执行,得到如下所示结果:
- bill@ubuntu:~/test/asm$ ./main
- a=1 b=2
- a=10 b=2
非常抱歉,这个结果比较意外,程序列表3-1中明明执行的是a+b操作,结果存放于a中,按道理来说,a应该等于3才是,正如情况一的执行结果那样。可是这里偏偏却不是这样子。为什么???先对main程序进行反汇编,我们来一看究竟,反汇编结果如程序列表3-2所示:
表3-2 情况3下的反汇编结果:
- 126 080483c4 <main>:
- 127 80483c4: push %ebp
- 128 80483c5: mov %esp,%ebp
- 129 80483c7: and $0xfffffff0,%esp
- 130 80483ca: push %ebx
- 131 80483cb: sub $0x1c,%esp
- 132 80483ce: mov $0x1,%eax
- 133 80483d3: mov $0x2,%ebx
- 134 80483d8: mov %ebx,%ecx
- 135 80483da: mov $0x80484e0,%edx
- 136 80483df: mov %ecx,0x8(%esp)
- 137 80483e3: mov %eax,0x4(%esp)
- 138 80483e7: mov %edx,(%esp)
- 139 80483ea: call 80482f4 <printf@plt>
- 140 80483ef: nop
- 141 80483f0: add %ebx,%eax
- 142 80483f2: nop
- 143 80483f3: mov %ebx,%edx
- 144 80483f5: mov $0x80484e0,%ecx
- 145 80483fa: mov %edx,0x8(%esp)
- 146 80483fe: mov %eax,0x4(%esp)
- 147 8048402: mov %ecx,(%esp)
- 148 8048405: call 80482f4 <printf@plt>
- 149 804840a: mov $0x0,%eax
- 150 804840f: add $0x1c,%esp
- 151 8048412: pop %ebx
- 152 8048413: mov %ebp,%esp
- 153 8048415: pop %ebp
- 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 #include "stdio.h"
- 2
- 3 int main(void)
- 4 {
- 5 register int a __asm("eax")=1;
- 6 register int b __asm("ebx")=2;
- 7
- 8 printf("a=%d b=%d\n", a, b);
- 9 a = 1;
- 10
- 11 __asm volatile("nop");
- 12 __asm volatile
- 13 ("addl %1,%0\n\t"
- 14 :"+r"(a)
- 15 :"r"(b)
- 16 );
- 17 __asm volatile("nop");
- 18
- 19 printf("a=%d b=%d\n", a, b);
- 20 return 0;
- 21
- 22 }
- 23
编译后得到的执行结果如下:
- bill@ubuntu:~/test/asm$ ./main
- a=1 b=2
- a=3 b=2
结果正确,恢复了正常,我们在对其进行反汇编,得到的代码如列表3-4所示:
表3-4 修正后的反汇编代码
- 126 080483c4 <main>:
- 127 80483c4: push %ebp
- 128 80483c5: mov %esp,%ebp
- 129 80483c7: and $0xfffffff0,%esp
- 130 80483ca: push %ebx
- 131 80483cb: sub $0x1c,%esp
- 132 80483ce: mov $0x1,%eax
- 133 80483d3: mov $0x2,%ebx
- 134 80483d8: mov %ebx,%ecx
- 135 80483da: mov $0x80484e0,%edx
- 136 80483df: mov %ecx,0x8(%esp)
- 137 80483e3: mov %eax,0x4(%esp)
- 138 80483e7: mov %edx,(%esp)
- 139 80483ea: call 80482f4 <printf@plt>
- 140 80483ef: mov $0x1,%eax
- 141 80483f4: nop
- 142 80483f5: add %ebx,%eax
- 143 80483f7: nop
- 144 80483f8: mov %ebx,%edx
- 145 80483fa: mov $0x80484e0,%ecx
- 146 80483ff: mov %edx,0x8(%esp)
- 147 8048403: mov %eax,0x4(%esp)
- 148 8048407: mov %ecx,(%esp)
- 149 804840a: call 80482f4 <printf@plt>
- 150 804840f: mov $0x0,%eax
- 151 8048414: add $0x1c,%esp
- 152 8048417: pop %ebx
- 153 8048418: mov %ebp,%esp
- 154 804841a: pop %ebp
- 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 #include "stdio.h"
- 2
- 3 int main(void)
- 4 {
- 5 register int a __asm("edi")=1;
- 6 register int b __asm("esi")=2;
- 7 printf("a=%d b=%d\n", a, b);
- 8
- 9 //a = 1;
- 10 __asm volatile("nop");
- 11 __asm volatile
- 12 ("addl %1,%0\n\t"
- 13 :"+a"(a)
- 14 :"b"(b)
- 15 );
- 16 __asm volatile("nop");
- 17
- 18 printf("a=%d b=%d\n", a, b);
- 19 return 0;
- 20
- 21 }
- 22
鉴于情况3的结果,这里将局部变量绑定非volatile寄存器,我们将焦点集中于嵌入式汇编中手工绑定C变量到寄存器的情况。程序编译运行结果正确,如情况1一样结果。通过实验发现,在程序列表4-1中的地13、14行,无论如何制定寄存器,其结构均正确,因为两个nop之间的嵌入式汇编不会再出现其他代码使用voaltile寄存器的情况,因此寄存器值的一致性可以得到保证。如果采用自动绑定形式,gcc会维持原有的绑定,如果手工绑定到具体寄存器,gcc也会照搬,只不过这样会增加一些指令,用于在先前绑定(局部变量绑定)寄存器和后来绑定寄存器(嵌入式汇编中C变量绑定)之间传递变量值。程序列表4-1中的代码反汇编结果如表4-2所示:
- 126 080483c4 <main>:
- 127 80483c4: push %ebp
- 128 80483c5: mov %esp,%ebp
- 129 80483c7: and $0xfffffff0,%esp
- 130 80483ca: push %edi
- 131 80483cb: push %esi
- 132 80483cc: push %ebx
- 133 80483cd: sub $0x14,%esp
- 134 80483d0: mov $0x1,%edi
- 135 80483d5: mov $0x2,%esi
- 136 80483da: mov %esi,%ecx
- 137 80483dc: mov %edi,%edx
- 138 80483de: mov $0x80484f0,%eax
- 139 80483e3: mov %ecx,0x8(%esp)
- 140 80483e7: mov %edx,0x4(%esp)
- 141 80483eb: mov %eax,(%esp)
- 142 80483ee: call 80482f4 <printf@plt>
- 143 80483f3: nop
- 144 80483f4: mov %edi,%eax
- 145 80483f6: mov %esi,%ebx
- 146 80483f8: add %ebx,%eax
- 147 80483fa: mov %eax,%edi
- 148 80483fc: nop
- 149 80483fd: mov %esi,%edx
- 150 80483ff: mov %edi,%eax
- 151 8048401: mov $0x80484f0,%ecx
- 152 8048406: mov %edx,0x8(%esp)
- 153 804840a: mov %eax,0x4(%esp)
- 154 804840e: mov %ecx,(%esp)
- 155 8048411: call 80482f4 <printf@plt>
- 156 8048416: mov $0x0,%eax
- 157 804841b: add $0x14,%esp
- 158 804841e: pop %ebx
- 159 804841f: pop %esi
- 160 8048420: pop %edi
- 161 8048421: mov %ebp,%esp
- 162 8048423: pop %ebp
- 163 8048424: ret
由程序列表4-2可以看出,虽然在嵌入式汇编中重新对变量进行了寄存器绑定,但是最终的计算结构还是要传回先前绑定的寄存器中edi。
至此,针对本文中示例代码的嵌入式汇编讨论告一段落。这里主要针对voaltile和非volatile两大类寄存器集在具体编译过程中的使用规则而展开讨论。由于讨论是限定在具体的程序代码之中,难以顾及方方面面,因此会存在这样或者那样的局限性,仅仅是个人的一些浅显总结,今后随着实验的深入而对本文进行扩充。如有错误之处,还请指导交流。
阅读(7482) | 评论(0) | 转发(1) |