在编程中,如果能够在汇编级别弄懂具体每条语句的内部实现,就会避免很多错误的发生,同时也能更加清楚合理的去调用内存空间。因此我们以一段代码为例,来从汇编语言的角度浅谈该段代码的函数调用关系和其具体的形参变量和实参变量之间的对应关系。
#include <stdio.h>
void fun1(double x);
int fun2(int a, int d, int c);
void fun1(double x) {
printf("%lf\n",x);
}
int fun2(int a, int b, int c) {
int tmp;
tmp = a + b + c;
++tmp;
return tmp;
}
int main() {
int one = 1;
int two = 2;
int three = 3;
fun1(3.14);
one += two;
printf("%d\n",fun2(one, two, three));
return 0;
}
==================================================================
其在VC环境下所产生的汇编源代码如下:
TITLE aboutVirable.c
.386P
include listing.inc
if @Version gt 510
.model FLAT
else
_TEXT SEGMENT PARA USE32 PUBLIC 'CODE'
_TEXT ENDS
_DATA SEGMENT DWORD USE32 PUBLIC 'DATA'
_DATA ENDS
CONST SEGMENT DWORD USE32 PUBLIC 'CONST'
CONST ENDS
_BSS SEGMENT DWORD USE32 PUBLIC 'BSS'
_BSS ENDS
_TLS SEGMENT DWORD USE32 PUBLIC 'TLS'
_TLS ENDS
FLAT GROUP _DATA, CONST, _BSS
ASSUME CS: FLAT, DS: FLAT, SS: FLAT
endif
PUBLIC _fun2
_TEXT SEGMENT
_a$ = 8
_b$ = 12
_c$ = 16
_tmp$ = -4
_fun2 PROC NEAR
; 6 : int fun2(int a, int b, int c) {
push ebp
mov ebp, esp
push ecx
; 7 : int tmp;
; 8 :
; 9 : tmp = a + b + c;
mov eax, DWORD PTR _a$[ebp]
add eax, DWORD PTR _b$[ebp]
add eax, DWORD PTR _c$[ebp]
mov DWORD PTR _tmp$[ebp], eax
; 10 : ++tmp;
mov ecx, DWORD PTR _tmp$[ebp]
add ecx, 1
mov DWORD PTR _tmp$[ebp], ecx
; 11 :
; 12 : return tmp;
mov eax, DWORD PTR _tmp$[ebp]
; 13 : }
mov esp, ebp
pop ebp
ret 0
_fun2 ENDP
_TEXT ENDS
PUBLIC _fun1
EXTRN _printf:NEAR
EXTRN __fltused:NEAR
_DATA SEGMENT
$SG355 DB '%lf', 00H
_DATA ENDS
_TEXT SEGMENT
_x$ = 8
_fun1 PROC NEAR
; 15 : void fun1(double x) {
push ebp
mov ebp, esp
; 16 : printf("%lf", x);
mov eax, DWORD PTR _x$[ebp+4]
push eax
mov ecx, DWORD PTR _x$[ebp]
push ecx
push OFFSET FLAT:$SG355
call _printf
add esp, 12 ; 0000000cH
; 17 : }
pop ebp
ret 0
_fun1 ENDP
_TEXT ENDS
PUBLIC _main
_DATA SEGMENT
$SG361 DB '%d', 0aH, 00H
_DATA ENDS
_TEXT SEGMENT
_one$ = -8
_two$ = -12
_three$ = -4
_main PROC NEAR
; 19 : int main() {
push ebp
mov ebp, esp
sub esp, 12 ; 0000000cH
; 20 : int one = 1;
mov DWORD PTR _one$[ebp], 1
; 21 : int two = 2;
mov DWORD PTR _two$[ebp], 2
; 22 : int three = 3;
mov DWORD PTR _three$[ebp], 3
; 23 :
; 24 : fun1(3.14);
push 1074339512 ; 40091eb8H
push 1374389535 ; 51eb851fH
call _fun1
add esp, 8
; 25 : one += two;
mov eax, DWORD PTR _one$[ebp]
add eax, DWORD PTR _two$[ebp]
mov DWORD PTR _one$[ebp], eax
; 26 : printf("%d\n", fun2(one, two, three));
mov ecx, DWORD PTR _three$[ebp]
push ecx
mov edx, DWORD PTR _two$[ebp]
push edx
mov eax, DWORD PTR _one$[ebp]
push eax
call _fun2
add esp, 12 ; 0000000cH
push eax
push OFFSET FLAT:$SG361
call _printf
add esp, 8
; 27 :
; 28 : return 0;
xor eax, eax
; 29 : }
mov esp, ebp
pop ebp
ret 0
_main ENDP
_TEXT ENDS
END
==================================================================
于此我做出如下几点解释:
写在前面:
1.esp--系统堆栈栈顶指针;
2.ebp--系统堆栈当前栈底指针;
3.根据系统堆栈向低端增长,push(入栈)会--esp; pop(出栈)会++esp;
第一:; 22 : int three = 3;
mov DWORD PTR _three$[ebp], 3
这句话是主函数中申请的一个局部变量,该变量的类型为int型,变量名为one; 通过"_three$ = -4"这句话可知," mov DWORD PTR _three$[ebp], 3" 等价于 " mov DWORD PTR -4[ebp], 3" 。这句汇编语言的意思是,将常量3赋值给以ebp-4为首地址的int空间,DWORD PTR就是int的对应。
第二:; 19 : int main() {
push ebp
mov ebp, esp
sub esp, 12 ; 0000000cH
这三句话的含义分别是:
1.将ebp的值入栈,esp会-4;2.将esp的值赋值给ebp,使得esp和ebp指向同一个空间,形成一个空栈,这个栈是被main()函数来使用的;3.将esp的值-12,也就是说,将栈的栈顶指针抬高12,用来存储三个局部变量one,two,three的值。
逐条分析下面这段代码:
; 19 : int main() {
push ebp
mov ebp, esp
sub esp, 12 ; 0000000cH
; 20 : int one = 1;
mov DWORD PTR _one$[ebp], 1
; 21 : int two = 2;
mov DWORD PTR _two$[ebp], 2
; 22 : int three = 3;
mov DWORD PTR _three$[ebp], 3
; 23 :
; 24 : fun1(3.14);
push 1074339512 ; 40091eb8H
push 1374389535 ; 51eb851fH
call _fun1
add esp, 8
第三:编译系统调用主函数开始,此时系统堆栈会分配出一段空间用来执行这段代码,首先有一个4字节空间用来存储原ebp的值,然后执行第二点中的三条语句,给主函数中所申请的三个4字节分配空间。从汇编语言可以看出,入栈的先后顺序依次为 three, two, one(-4,-8,-12)。将调用fun1()中所传入的实参变量3.14入栈,由于fun1函数的行参变量类型是double型的,故此时esp会向上飘8个字节。
第四:执行"call fun1" , call指令内部会执行push eip的操作,保护eip的值,以便返回时能继续执行主函数的下一条指令,这里就是"add esp, 8";而且接着call会执行mov eip, fun1;(保存fun1函数的eip值)这里的fun1就是子函数fun1的首地址常量。于是,CPU将取出fun1函数的第一条指令开始执行。
; 15 : void fun1(double x) {
push ebp
mov ebp, esp
这段代码的意思是:首先,将main()函数中的栈底指针ebp入栈,esp再向上飘4个字节,将esp的值赋值给ebp,使得esp和ebp指向同一个空间,此时这段空间也就相当于是fun1()函数自己一个栈。
_x$ = 8
; 16 : printf("%lf", x);
mov eax, DWORD PTR _x$[ebp+4]
push eax
mov ecx, DWORD PTR _x$[ebp]
push ecx
push OFFSET FLAT:$SG355
call _printf
add esp, 12 ; 0000000cH
; 17 : }
接下来的这段代码的意思是,将以ebp+4+8为首地址的4字节空间的值赋值给eax寄存器,然后将eax的值入栈,再将以ebp+8为首地址的4字节空间的值赋值给ecx寄存器,然后将ecx的值入栈,这个操作的目的是将fun1()函数的实参变量3.14这个值复制一份,入栈,将fun1()函数的实参变量和行参变量对应起来,然后再次调用一个系统函数printf(),将"%lf"的首地址入栈,esp再次向上飘4个字节,调用完printf()函数之后,esp回落12个字节(一个首地址的4个字节加上一个行参变量8个字节)。
。。。未完待续。。。