目录
源代码变为可执行文件的过程
栈是什么
入栈过程
出栈过程
C语言函数调用栈
寄存器
EAX,EBX,ECX,EDX
寄存器使用约定
栈帧
调用的解释
释放的解释
堆栈操作
函数调用的操作
堆栈的主要指令
push/pop
call/leave/ret
函数序和函数跋
我们在数据结构中学习过栈
最基本的 前进后出 我们pwn中的栈溢出就是这里的运用栈的特点
源代码变为可执行文件的过程
在高级语言进行汇编的时候 例子为gcc编译
这四个步骤
gcc处理c语言过程
预处理 :对头文件进行处理 删除注释等处理
编译:将预处理完的代码进行语法分析 语法优化 语义分析 然后生成 汇编语言
汇编 : 通过汇编指令 和机器码的的对照表进行翻译
转换为机器语言
链接:分为静态链接和动态链接 gcc默认使用动态
静态链接 : 将目标代码直接复制到可执行文件代码中 生成的代码会很大 但是可以运行在没有任何依赖库的机器上
动态链接 :需要的函数从共享库中动态分配到内存中,程序执行的时候会去调用函数,生成的代码会较小,但是需要运行在有共享库的机器上
链接是将汇编后还没有解析的符号 通过库进行配对 然后再生成可执行文件的过程
我们了解完 文件是如何变为可执行文件后
我们要知道 我们汇编完 栈是什么
栈是什么
在汇编后 程序中不止包含着栈 还有很多东西
可执行文件在运行的时候开辟的虚拟内存空间
栈就是虚拟内存空间一部分 通常用来存储函数调用信息和局部变量
要注意
程序的栈是从进程地址空间的高地址向低地址增长的
入栈过程
出栈过程
C语言函数调用栈
我们现在来具体说明C语言函数调用栈
程序的执行 我们可以把他看做一个连续的过程
程序执行完后要回到调用函数的下一条指令(在call后面的那一条)继续执行
执行函数puts
执行完后
是回到下一个add的
所以我们来梳理一下函数堆栈的过程
1. 主函数调用函数 call 函数名
2. 被调用函数通过 mov ebp esp 来保存现在的栈顶
3. 被调用函数通过堆栈的方式 存入所需要的局部变量和寄存器
4. 被调用函数执行完 将返回值存入某个寄存器 并且执行ret操作 会将栈顶弹出 并且把控制器返回主函数
ebp是栈底
esp是栈顶
mov ebp esp 是因为要执行函数了 所以把栈底ebp指向call 然后开始进行堆栈
这里堆栈的过程我们后面进行说
我们先看看 调用函数必不可少的 寄存器
寄存器
寄存器是处理器用来加工,运行函数必不可少的东西
用于存放函数的 数据和指令
所以函数调用栈 离不开 寄存器
intel 32 包括8个4字节的寄存器
前6个寄存器均可作为通用寄存器使用
但是某些功能又需要特定的寄存器来使用
例如
函数返回值通常保存在%eax
开始解读
EAX,EBX,ECX,EDX
这四个可以进行拆分
可以分为两个独立的十六位寄存器
高寄存器/高地址 : AX BX CX DX
低寄存器/低地址 :AL,AH BL,BH CL,CH DL,DH
当EAX为低寄存器时 又可以拆分为 两个独立的八位寄存器
高字节 : AH,BH,CH,DH
低字节 : AL,BL,CL,DL
在汇编语言的使用中 会使用 %或者直接调用
mov $5 %eax
mov eax 5
两个都是将立即数5 赋值给eax
在 64位和32位中 寄存器的称呼也不一样
64位中 以R开头 例如 RAX RBX
32位中 以E开头 例如 EAX EBX
寄存器使用约定
EAX,ECX,EDX 为主函数调用的寄存器 在代码执行后 主函数希望有这些寄存器的控制
EBX,ESI,EDI 为被调用函数的寄存器 所以主函数如果使用了这些寄存器
要先将这些寄存器的值压入栈内 然后被调用函数使用这些寄存器
然后再把主函数的值 返回给这些寄存器 因为这些寄存器 主函数可能也要用
ESP EBP 被调用函数要保持的寄存器 并且在被调用函数运行完 要恢复为调用前的样子
就是恢复为主函数的栈帧
栈帧
在执行程序的时候 栈不一定是一个函数 而是很多函数的嵌套
同一时刻 栈内会有很多函数的信息
每一个没有执行完毕的函数都占有一片连续的空间 我们把这叫做栈帧
栈帧是堆栈的片段
在调用函数的时候
1.逻辑栈帧压入堆栈内 就是一个函数的栈帧
2.函数返回的时候 逻辑栈帧被弹出堆栈
逻辑栈帧里面存放着 函数的参数 局部变量 还有恢复为前一个栈帧所需要的数据
栈帧的边界是esp和ebp确定
EBP为栈底 在栈内位置固定
ESP为栈顶 每次有新的参数加入 都要对栈顶做减法
注意 这里的主函数和被调用函数都是该函数的栈帧
其中参数和局部变量可以没有
这里可以发现函数入栈的顺序
实参n-1 -->主函数返回地址 --> 主函数的ebp地址 --> 局部变量1-n
这里给出一个我学习的困惑 但是已经解决了 就是栈溢出
从这里就可以清晰明了是如何栈溢出
记住我的图是从下面进行入栈和出栈的
参数:垃圾字符填充
局部变量 垃圾字符填充
ebp 垃圾字符填充
返回地址 : shellcode
调用的解释
1.将 实参 由 N-1 进行入栈
2.进入被调用函数
3.把主函数的ebp的值压入栈内
4.把主函数的esp的值 赋值给 被调用函数的ebp 作为被调用函数的栈底
5.esp进行规划空间
这个时候 被调用函数的ebp
这里的前一个栈帧的地址 就是主函数地址
向上可以得到主调函数的返回地址,实参
向下可以得到局部变量和参数
释放的解释
在函数执行完后
1.将被调用函数的ebp 赋值给 被调用函数的esp
这样 局部变量和参数 都被释放了
2.把前一个栈帧的ebp地址弹出给EBP
让他返回没有调用函数前的栈底
3.esp继续上移 返回到主函数的栈帧
注意这些
1. 在当前函数执行的时候 EBP始终不改变位置 都是在栈底
2.在函数调用前 ESP指向EBP 即他既指向栈底 又指向栈顶
3.当函数开始执行后 ESP 会一直指向该函数的栈顶
4.如果在函数中开始了另一个函数
那么就会把 第一个函数的EBP作为 旧的EBP压入栈内 然后 新的函数开始从第一个函数的ESP开始压入栈
如果主函数中需要保存 被调用函数的寄存器和临时变量 那么他的栈图应该是
堆栈操作
函数调用的操作
(1)主函数将被调用函数的参数 按照约定压入栈内 这个时候 主函数的esp开始移动
x86是直接压入栈内 x64是先将6个通用寄存器存储 后才进行压入栈
(2)主函数把控制器交给被调用函数(call指令)函数的返回地址(call自动压入)压入栈
返回地址是call函数下一条指令
(3)如果有必要 被调用函数会设置ebp,然后保存被调用函数保存的寄存器
(4)被调用函数通过修改esp来为局部变量进行开辟空间 并且存入局部变量和临时变量
(5)被调用函数要调用主函数传入的值 如果被调用函数返回一个值 多半是从EAX存入
(6)被调用函数执行结束 就先进行修改esp=ebp 然后把旧ebp赋值给ebp 然后就能释放空间
(7)恢复被调用函数控制的寄存器
(8)把控制器交还给主函数 (这个操作有可能清除参数)
(9)主函数清除之前存入的参数 把esp修改到(1)前的值
通过9个步骤 我们的函数调用和使用完函数后的操作 就结束了
堆栈的主要指令
push/pop
push 压入栈内 esp-4个字节 以字节为单位将寄存器数据压入栈内
从高字节到低字节依次存入
esp-1 esp-2 esp-3 esp -4的地址上
pop 出栈 栈顶的数据存储到寄存器中 esp+4个字节
很显然
存入数据 栈顶变小 提出数据 栈顶变大
esp
但是esp始终是指向 下一条数据的
call/leave/ret
eip寄存器
EIP是x86架构CPU中的一个寄存器,它存储当前正在执行的指令的地址。EIP代表"Extended Instruction Pointer",可以理解为指向下一条要执行的指令的地址。当CPU执行完一条指令后,会根据当前EIP的值计算出下一条指令的地址,并开始执行下一条指令。程序员可以通过修改EIP的值来改变指令执行的顺序,从而实现程序跳转等操作。
call : 指令寄存器EIP会先将 call指令下一条压入栈内 然后再重新指向被调用函数开始处
leave:用于恢复主函数的栈帧来准备返回
类似于两条汇编指令
mov esp,ebp 把栈底赋值给栈顶 释放空间 返回到主函数
pop ebp 因为栈顶存放的是上一栈帧的地址 所以pop出ebp 就可以返回上一个栈帧地址
ret:和call配套使用 用于从函数或过程返回
从栈顶弹出 返回地址(下一条指令地址)给eip 程序开始指向下一条指令地址
esp会指向 被调用函数返回主函数的地址处
函数序和函数跋
函数调用之初常一同出现 的 我们叫做函数序 类似上面的 (3)(4)
函数调用最后常一同出现 的 我们叫做函数跋 类似上面的 (6)(7)(8)
这里给出常用的函数序和函数跋的汇编