C 0x01(C语言是如何进行函数调用的)
-
在上一篇文章中我说错了一件事,其实没有pc寄存器,pc全称是program counter (程序计数器),他是一个抽象的概念,在x86/x64下是ip的数值(也不是很准确,在以后的文章中我们不必纠结这些事情都管他叫做PC)
- rbp 寄存器大小
- pc 寄存器大小
在函数调用的时候会在栈中记录下函数调用后下一个指令的地址(pc),还会记录栈基址(rbp),这些数据记录在哪里?肯定就是内存里啦!我们再仔细看一眼汇编代码
//C int testfunc(){ int a=0; } int testfunc2() { testfunc(); }
//asm testfunc: push rbp mov rbp, rsp mov DWORD PTR -4[rbp], 0 nop pop rbp ret testfunc2: push rbp mov rbp, rsp mov eax, 0 call testfunc nop pop rbp ret
看汇编代码我们是不是看不哪里压栈了,这时候就要介绍一下call指令的执行过程
- 首先call指令会将PC入栈
- 然后会有一个jmp跳转到要执行的地址
说起来可能很抽象,下面用一张图来说明他的过程
这张图片应该就可以很清晰地描绘call指令都干了些啥了吧!
由此可见,在函数调用的过程中程序的返回地址存在栈栈中,也就是栈上的数据会影响程序的运行过程.试想如果我们修改了栈上特定位置的数值,并修改成特定的数值,那么我们就可以让程序走到意想不到的地方(栈溢出).
32位(x86情况下)
先上代码
int f=0; int c=0; int testfunc(){ int a=0; f=1; return c; } int testfunc2() { testfunc(); }
f: .zero 4 c: .zero 4 testfunc: push ebp mov ebp, esp sub esp, 16 call __x86.get_pc_thunk.ax add eax, OFFSET FLAT:_GLOBAL_OFFSET_TABLE_ mov DWORD PTR -4[ebp], 0 mov DWORD PTR f@GOTOFF[eax], 1 mov eax, DWORD PTR c@GOTOFF[eax] leave ret testfunc2: push ebp mov ebp, esp call __x86.get_pc_thunk.ax add eax, OFFSET FLAT:_GLOBAL_OFFSET_TABLE_ call testfunc nop pop ebp ret __x86.get_pc_thunk.ax: mov eax, DWORD PTR [esp] ret
我们发现多了一个这个函数 __x86.get_pc_thunk.ax 这个是干什么的呢?我们来分析一下
在testfunc2中有这样一条指令call __x86.get_pc_thunk.ax
首先call指令会将PC寄存器入栈,然后在 __x86.get_pc_thunk.ax 函数中进行了
eax, DWORD PTR [esp]
这个操作,esp是栈指针指向的是栈顶部也就是刚刚push的数据也就是PC寄存器的数值了,所以说这个函数是获取下一条指令的地址的,原因是因为x86架构下没有获取PC寄存器的指令只能用这种方式获取了.
PS:为什么要这么做
因为有一个功能叫做PIE(地址随机化)这个可以有效增加栈溢出导致REC风险
接着来看add eax, OFFSET FLAT:_GLOBAL_OFFSET_TABLE_
这个 FLAT:GLOBAL_OFFSET_TABLE 是全局静态变量表所对应add指令的偏移量,这个是在编译时期确定的,所以这个代码的意思是找到全局静态变量表的地址并储存到eax寄存器中,为了方便我在下文分析函数调用的时候会关闭pie得到相对简单的汇编代码
int testfunc(int g){ g=8; } int testfunc2() { testfunc(2); }
testfunc: push ebp mov ebp, esp mov DWORD PTR [ebp+8], 8 nop pop ebp ret testfunc2: push ebp mov ebp, esp push 2 call testfunc add esp, 4 nop leave ret
可以看到在x86下只有栈传参没有寄存器传参,其余的和x64下的是相同的
PS:如果你想复现的话记得加入-m32 --no-pie这两个编译参数 一个是目标平台是32位的,一个是不使用pie
-
PC寄存器基本都是有限制的, 一般可以通过间接的方式修改。而且它一般指一个逻辑上的寄存器, 可能每个架构下实现和名字有所不同。总之, 感觉记住它是存储CPU下一条要执行的指令(在机器语言中)的内存地址就可以了
架构 PC 名称 可否直接读取 可否直接写入 位数 x86 EIP
间接 32 x86_64 RIP
间接 64 ARM32 R15
/PC
32 ARM64 PC
️ 限制
64 RISC-V pc
(模拟器)
️ 部分支持
32/64 MIPS PC
32 PowerPC NIP
(特权)
32/64