【C数据结构与算法】用系统堆栈,汇编代码,寄存器来理解C形参和实参
说说C语言中的形参和实参。
据我的学习,形参和实参 1.个数相等;
2.位置关系对应;
3.类型一致;
4.效果等于 值传递(实参表达式的值,复制一份,传递给形参);
下面有个例子:实现一个交换两个整形变量的交换
#include <stdio.h>
void exchange (int,int);
void exchange (int *one,int *another)
{
int tmp;
tmp = *one;
*one = *another;
*another = tmp;
}
int main ()
{
int num1;
int num2;
scanf ("%d",&num1);
scanf ("%d",&num2);
exchange (&num1,&num2);
printf ("结果为:%d,%d\n",num1,num2);
return 0;
}
从内存角度分析:
- num1 入栈,栈顶指针上移4字节;
- num2 入栈,栈顶指针上移4字节;
- 保存主函数现场信息,栈顶指针上移8字节;
- 将num2的地址信息入栈,栈顶指针上移4字节;
- 将num1的地址信息入栈,栈顶指针上移4字节;
- 将num1的地址信息(首地址)传递给one;
- 将num2的地址信息(首地址)传递给another; !!!实参表达式计算出的值在堆栈里所占的空间就是形参变量的空间,形参和实参是值传递的关系,形参所占的空间就是占用了之前系统分配给实参的空间!
- tmp入栈,栈顶指针上移4字节;
- 将one所指向空间的值(num1的值)赋值给tmp;
- 将another所指向空间的值(num2的值)赋值给one所指向的空间;
- 将tmp赋值给another所指向的空间;
- 栈顶指针下移12个字节,即指向了 主地址现场信息 ;
- 继续执行主函数。。。输出。。
在此提一点:形参与实参是单向传递,对行参变量的任何修改都不会更改实参表达式原本的值!
如果继续向本质去探寻,就不得不扯上关于汇编的内容,命令行编译该程序(cl /FAs exchange.c),生成格式为asm的文件,打开该文件:
TITLE exchange.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 _exchange
_TEXT SEGMENT
_one$ = 8
_another$ = 12
_tmp$ = -4
_exchange PROC NEAR
; 6 : {
push ebp
mov ebp, esp
push ecx
; ebp值入栈(将main() ebp保护起来)
; 再次形成“空栈”,就是exchange()的空栈
; 7 : int tmp;
; 8 : tmp = *one;
mov eax, DWORD PTR _one$[ebp] ; mov eax, 4B ebp[4]
mov ecx, DWORD PTR [eax]
mov DWORD PTR _tmp$[ebp], ecx
; 9 : *one = *another;
mov edx, DWORD PTR _one$[ebp]
mov eax, DWORD PTR _another$[ebp]
mov ecx, DWORD PTR [eax]
mov DWORD PTR [edx], ecx
; 10 : *another = tmp;
mov edx, DWORD PTR _another$[ebp]
mov eax, DWORD PTR _tmp$[ebp]
mov DWORD PTR [edx], eax
; 11 : }
mov esp, ebp
pop ebp ; 用栈顶当前的值(main() ebp)赋值给ebp,并esp--4
; 使ebp回到main函数的ebp状态
ret 0
; mov eip,栈顶
; 恢复CPU运行顺序,回到主函数被中断时的状态
_exchange ENDP
_TEXT ENDS
PUBLIC _main ;主函数
EXTRN _printf:NEAR
EXTRN _scanf:NEAR
_DATA SEGMENT ;数据区
$SG347 DB '%d', 00H
ORG $+1
$SG348 DB '%d', 00H
ORG $+1
$SG349 DB 0bdH, 0e1H, 0b9H, 0fbH, 0ceH, 0aaH, ':%d,%d', 0aH, 00H
_DATA ENDS
_TEXT SEGMENT
_num1$ = -4
_num2$ = -8
_main PROC NEAR
; 14 : {
push ebp ; 将ebp值入栈,esp会-4
mov ebp, esp ; 将esp的值赋值给ebp; So,esp和ebp此时指向同一个空间,即 形成了一个空栈,每一个函数都有
sub esp, 8 ; 将esp的值-8, 意思是将栈顶指针抬高8B,意味着栈顶指针和栈底指针之间有了8B的间隔,8B应该为两个int空间;就是num1和num2
; 调用主函数的“函数”(操作系统的代码),他的ebp被保护起来(通过push和mov),原栈底空间被保护起来
; 15 : int num1;
; 16 : int num2;
; 17 : scanf ("%d",&num1);
lea eax, DWORD PTR _num1$[ebp] ;-4[ebp] 3[a] = a[3]
; lea eax,DWORD PTR -4[ebp] <=> lea eax,DWORD PTR ebp[-4]
; eax存入以ebp-4为首地址的int空间
; DWORD_PTR就是int的对应
; 系统堆栈向低端增长,意思是,入栈(push)会--esp,而出栈(pop)会++esp
; esp:系统堆栈栈顶指针。ebp:系统堆栈栈底指针
; ebp这个栈底指针的上方,隔4B空间,是主函数的局部变量num1的空间
; 再看主函数局部变量num2,(_num2$ = -8),同理,他离栈底指针更远,间隔8B空间
; !!!这就说明栈底和第一个局部变量(num1)之间存在一个4B空间!!!
; 5. 这4B空间就是num1的空间!!! 因为系统堆栈中越往上地址值越小,越往下地址值越大!
push eax
push OFFSET FLAT:$SG347
call _scanf
add esp, 8
; 18 : scanf ("%d",&num2);
lea ecx, DWORD PTR _num2$[ebp]
push ecx
push OFFSET FLAT:$SG348
call _scanf
add esp, 8
; 19 :
; 20 : exchange (&num1,&num2); ; 调用函数
lea edx, DWORD PTR _num2$[ebp]
push edx
lea eax, DWORD PTR _num1$[ebp]
push eax
; 将num2和num1首地址入栈;
call _exchange
; call指令内部会执行push eip 的操作!!! 保护eip的值,以便返回时能继续执行主调函数的下一条指令!
; 这条指令就是下面的add esp, 8; 接着call会执行mov eip, exchange; exchange就是子函数exchange的首地址常量
; CPU将取出exchange函数的第一条指令开始执行,即,开始执行子函数exchange
; 这里的call由于执行的是push eip;所以,此时esp又会-4
add esp, 8 ; esp下落8字节,此时正好回到调用前状态
; 21 : printf ("结果为:%d,%d\n",num1,num2);
mov ecx, DWORD PTR _num2$[ebp]
push ecx
mov edx, DWORD PTR _num1$[ebp]
push edx
push OFFSET FLAT:$SG349 ; 字符串常量的本质是该字符串常量的首地址常量
call _printf
add esp, 12 ; 0000000cH
; 22 : return 0;
xor eax, eax
; 23 : }
mov esp, ebp
pop ebp
ret 0
_main ENDP
_TEXT ENDS
END
; ebp值入栈(将main() ebp保护起来)
; 再次形成“空栈”,就是exchange()的空栈
; 7 : int tmp;
; 8 : tmp = *one;
mov eax, DWORD PTR _one$[ebp] ; mov eax, 4B ebp[4]
mov ecx, DWORD PTR [eax]
mov DWORD PTR _tmp$[ebp], ecx
; 9 : *one = *another;
mov edx, DWORD PTR _one$[ebp]
mov eax, DWORD PTR _another$[ebp]
mov ecx, DWORD PTR [eax]
mov DWORD PTR [edx], ecx
; 10 : *another = tmp;
mov edx, DWORD PTR _another$[ebp]
mov eax, DWORD PTR _tmp$[ebp]
mov DWORD PTR [edx], eax
; 11 : }
mov esp, ebp
pop ebp ; 用栈顶当前的值(main() ebp)赋值给ebp,并esp--4
; 使ebp回到main函数的ebp状态
ret 0
; mov eip,栈顶
; 恢复CPU运行顺序,回到主函数被中断时的状态
_exchange ENDP
_TEXT ENDS
PUBLIC _main ; 主函数
EXTRN _printf:NEAR
EXTRN _scanf:NEAR
_DATA SEGMENT ; 数据区
$SG347 DB '%d', 00H
ORG $+1
$SG348 DB '%d', 00H
ORG $+1
$SG349 DB 0bdH, 0e1H, 0b9H, 0fbH, 0ceH, 0aaH, ':%d,%d', 0aH, 00H
_DATA ENDS
_TEXT SEGMENT
_num1$ = -4
_num2$ = -8
_main PROC NEAR
; 14 : {
push ebp ; 将ebp值入栈,esp会-4
mov ebp, esp ; 将esp的值赋值给ebp; So,esp和ebp此时指向同一个空间,即 形成了一个空栈,每一个函数都有
sub esp, 8 ; 将esp的值-8, 意思是将栈顶指针抬高8B,意味着栈顶指针和栈底指针之间有了8B的间隔,8B应该为两个int空间;就是num1和num2
; 调用主函数的“函数”(操作系统的代码),他的ebp被保护起来(通过push和mov),原栈底空间被保护起来
; 15 : int num1;
; 16 : int num2;
; 17 : scanf ("%d",&num1);
lea eax, DWORD PTR _num1$[ebp] ; -4[ebp] 3[a] = a[3]
; lea eax,DWORD PTR -4[ebp] <=> lea eax,DWORD PTR ebp[-4]
; eax存入以ebp-4为首地址的int空间
; DWORD_PTR就是int的对应
; 系统堆栈向低端增长,意思是,入栈(push)会--esp,而出栈(pop)会++esp
; esp:系统堆栈栈顶指针。ebp:系统堆栈栈底指针
; ebp这个栈底指针的上方,隔4B空间,是主函数的局部变量num1的空间
; 再看主函数局部变量num2,(_num2$ = -8),同理,他离栈底指针更远,间隔8B空间
; !!!这就说明栈底和第一个局部变量(num1)之间存在一个4B空间!!!
; 5. 这4B空间就是num1的空间!!! 因为系统堆栈中越往上地址值越小,越往下地址值越大!
push eax
push OFFSET FLAT:$SG347
call _scanf
add esp, 8
; 18 : scanf ("%d",&num2);
lea ecx, DWORD PTR _num2$[ebp]
push ecx
push OFFSET FLAT:$SG348
call _scanf
add esp, 8
; 19 :
; 20 : exchange (&num1,&num2); // 调用函数
lea edx, DWORD PTR _num2$[ebp]
push edx
lea eax, DWORD PTR _num1$[ebp]
push eax
; 将num2和num1首地址入栈;
call _exchange
; call指令内部会执行push eip 的操作!!! 保护eip的值,以便返回时能继续执行主调函数的下一条指令!
; 这条指令就是下面的add esp, 8; 接着call会执行mov eip, exchange; exchange就是子函数exchange的首地址常量
; CPU将取出exchange函数的第一条指令开始执行,即,开始执行子函数exchange
; 这里的call由于执行的是push eip;所以,此时esp又会-4
add esp, 8 ; esp下落8字节,此时正好回到调用前状态
; 21 : printf ("结果为:%d,%d\n",num1,num2);
mov ecx, DWORD PTR _num2$[ebp]
push ecx
mov edx, DWORD PTR _num1$[ebp]
push edx
push OFFSET FLAT:$SG349 ; 字符串常量的本质是该字符串常量的首地址常量
call _printf
add esp, 12 ; 0000000cH
; 22 : return 0;
xor eax, eax
; 23 : }
mov esp, ebp
pop ebp
ret 0
_main ENDP
_TEXT ENDS
END
建议根据汇编代码和我给的理解和注释画图理解!