文章目录
- 函数栈帧
- 栈帧创建
- 栈帧销毁
- 根据栈帧关系更改值
- 拓展
- 可变参数列表
- 基本原理
- 整形提升
- 命令行参数
- 打印环境变量
函数栈帧
int MyAdd(int a, int b)
{
int c = 0;
c = a + b;
return c;
}
int main()
{
int x = 0xA;
int y = 0xB;
int z =MyAdd(x,y);
system("pause");
return 0;
}
--认识寄存器
eax:通用寄存器,保留临时数据,常用于返回值
ebx:通用寄存器,保留临时数据
ebp:栈底寄存器(bottom底部)
esp:栈顶寄存器
eip:指令寄存器,保存当前指令的下一条指令的地址
--汇编指令
mov:数据转移指令
push:数据入栈,同时esp栈顶寄存器也要发生改变
pop:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变
sub:减法命令
add:加法命令
call:函数调用,1. 压入返回地址 2. 转入目标函数
jump:通过修改eip,转入目标函数,进行调用
ret:恢复返回地址,压入eip,类似pop eip命令
- call压入返回地址?
根本原因是:函数是可能调用完毕的,就需要返回.所以需要将call命令下一条地址压栈.
- EIP中被修改为jmp跳转函数的地址到jmp,jmp再跳转到函数地址.然后EIP中再是目标函数地址.
栈帧创建
栈帧销毁
\1. 调用函数之前,需要先形成临时拷贝并且通过寄存器压入栈中,形参形成过程是从右向左的.
\2. 临时空间的开辟,是在对应函数栈帧内部开辟的
\3. 函数调用完毕,栈帧结构被释放掉
\4. 临时变量具有临时性的本质:栈帧具有临时性
\5. 调用函数是有成本的,成本体现在时间和空间上,本质是形成和释放栈帧有成本
\6. 函数调用,因拷贝所形成的临时变量,变量和变量之间的位置关系是有规律的
根据栈帧关系更改值
int MyAdd(int a, int b)
{
printf("Before:%d\n",b);
*(&a + 1) = 100;
printf("After:%d\n", b);
/*int c = 0;
c = a + b;*/
return 0;
}
int main()
{
int x = 0xA;
int y = 0xB;
int z =MyAdd(x,y);
system("pause");
return 0;
}
拓展
将main函数ebp地址栈帧相对位置更改实现回原本main函数时去调用其他函数.
只能通过控制停止时间观察现象,具体实现中间插入一个函数的运行还需记录main函数栈帧中原先的地址,由于随机栈等保护措施的存在,现在暂时无法完成.
void bug()
{
printf("You can see me\n");
Sleep(10000);
}
int MyAdd(int a, int b)
{
printf("MyAdd be called!\n");
*(&a - 1) = (int)bug;
return 0;
}
int main()
{
int x = 0xA;
int y = 0xB;
int z =MyAdd(x,y);
system("pause");
return 0;
}
可变参数列表
函数传参求两个数较大值,形参可以确定的两个.
int FindMax(int x, int y)
{
if (x > y)
return x;
else
return y;
}
int main()
{
int x = 0;
int y = 0;
printf("Please Eneter Two Data# ");
scanf("%d %d", &x, &y);
int max = FindMax(x, y);
printf("max = %d\n", max);
system("pause");
return 0;
}
如果函数传参传多个值,需要在多个值中确定最大值,参数个数不确定,编写函数时形参个数无法确定,所以C提供了可变参数列表.
如果没有形式参数,可以给函数传参么?可以的
在C中,只要发生函数调用并且传递参数,必定形成临时拷贝
所谓的临时拷贝本质就是在栈帧中形成的,从右向左依次形成临时拷贝.
- 可变参数列表至少要有一个参数
基本原理
- 通过指针操作以及栈帧中临时变量相对位置获取
int FindMax(int num, ...)
{
va_list arg;//char* 类型指针
va_start(arg,num);//让arg指针指向可变参数部分
int max = va_arg(arg,int);
for (int i = 0; i < num - 1; i++)
{
int cur = va_arg(arg, int);
if (max < cur)
{
max = cur;
}
}
va_end(arg);//指针置空
return max;
}
int main()
{
int max = FindMax(5, 0x11, 0x22, 0x33, 0x44, 0x65);
printf("max=%d\n", max);
return 0;
}
整形提升
-
可变参数中,如果是短整型,一般都需要进行int整形提升.movsx.即使是char类型,在压栈的时候都是4字节.
通过查看汇编,我们看到,在可变参数场景下:
\1. 实际传入的参数如果是char,short,float,编译器在编译的时候,会自动进行提升(通过查看汇编,我们都能看到)
\2. 函数内部使用的时候,根据类型提取数据,更多的是通过int或者double来进行
int FindMax(int num, ...)
{
va_list arg; //定义可以访问可变参数部分的变量,其实是一个char*类型
va_start(arg, num); //使arg指向可变参数部分
int max = va_arg(arg, int); //根据类型,获取可变参数列表中的第一个数据
for (int i = 0; i < num - 1; i++) {//获取并比较其他的
int curr = va_arg(arg, int);
if (max < curr) {
max = curr;
}
}
va_end(arg); //arg使用完毕,收尾工作。本质就是讲arg指向NULL
return max;
}
int main()
{
char a = '1'; //ascii值: 49
char b = '2'; //ascii值: 50
char c = '3'; //ascii值: 51
char d = '4'; //ascii值: 52
char e = '5'; //ascii值: 53
int max = FindMax(5, a, b, c, d, e);
printf("max = %d\n", max);//max=53
system("pause");
return 0;
}
- 可变参数必须从头到尾逐个访问。如果你在访问了几个可变参数之后想半途终止,这是可以的,但是,如果你想一开始就访问参数列表中间的参数,那是不行的。
- 参数列表中至少有一个命名参数。如果连一个命名参数都没有,就无法使用 va_start 。
- 这些宏是无法直接判断实际存在参数的数量。
- 这些宏无法判断每个参数的是类型。
- 如果在 va_arg 中指定了错误的类型,那么其后果是不可预测的。
\1. 可变参数列表对应的函数,最终调用也是函数调用,也要形成栈帧
\2. 栈帧形成前,临时变量是要先入栈的,根据之前所学,参数之间位置关系是固定的
\3. 通过上面汇编的学习,发现了短整型在可变参数部分,会默认进行整形提升,那么函数内部在提取该数据的时候,就要考虑提升之后的值,如果不加考虑,获取数据可能会报错或者结果不正确
_INTSIZEOF(n)的意思:计算一个最小数字x,满足 x>=n && x%4==0,其实就是一种4字节对齐的方式是什么:
比如n是:1,2,3,4 对n进行向 sizeof(int) 的最小整数倍取整的问题 就是 4
比如n是:5,6,7,8 对n进行向 sizeof(int) 的最小整数倍取整的问题 就是 8
所以,简洁版:(n+4-1) & ~(4-1)
原码版:( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
命令行参数
main函数也是一个函数,其实也可以携带参数的
int main( int argc, char *argv[ ], char *envp[ ] )
{
program-statements
}
那这里是有三个参数的。
第一个参数: argc 是个整型变量,表示命令行参数的个数(含第一个参数)。
第二个参数: argv 是个字符指针的数组,每个元素是一个字符指针,指向一个字符串。这些字符串就是命令行中的每一个参数(字符串)。
第三个参数: envp 是字符指针的数组,数组的每一个原元素是一个指向一个环境变量(字符串)的字符指针.
打印环境变量
int main(int argc, char* argv[], char* env[])
{
for (int i = 0; env[i]; i++)//env[i]结尾就是NULL
{
printf("env[%d]:%s\n", i, env[i] );
}
}