平时在读写代码过程中屡屡会遇到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两大类寄存器集在具体编译过程中的使用规则而展开讨论。由于讨论是限定在具体的程序代码之中,难以顾及方方面面,因此会存在这样或者那样的局限性,仅仅是个人的一些浅显总结,今后随着实验的深入而对本文进行扩充。如有错误之处,还请指导交流。