access 调用 webbrowser_函数调用过程与栈帧结构 - wuli涛涛
进程内存布局
32 位保护模式下 Linux 中进程的内存布局如下:
0xFFFFFFFF ------> +----------------------+ <--+ | | | | OS Kernel | | 1GB| | |0xC0000000 ------> +----------------------+ <--+ | | | || | stack | |v | | |+----------------------+ || | |+----------------------+ || Memory Mapping Region| |0x40000000 ------> +----------------------+ || | |+----------------------+ |^ | | || | heap | | 3GB| | | |+----------------------+ || Read/Write Segments | || .bss | || .data | |+----------------------+ || Read-only Segments | || .init .text .rodata | || | |0x08048000 ------> +----------------------+ || reserved | |0x00000000 ------> +----------------------+ <--+
32 位下寻址空间为 4GB,其中高地址的 1GB 是给操作系统内核使用的(Windows 中默认为 2GB),称为内核空间(Kernel Space),剩余的 3GB 分配给应用程序使用,称为用户空间(User Space)。当进程运行在内核空间时就处于内核态(Kernel Mode),当进程运行在用户空间时就处于用户态(User Mode)。
区分用户态和内核态是为了提高系统的稳定性和安全性,使操作系统能够控制资源的访问,能够防止应用程序执行一些危险的指令。计算机体系结构中,在硬件上提供不同的特权态,即 Rings Protection,如 Intel CPU 有 4 个特权级:Ring0、Ring1、Ring2、Ring3,一般操作系统只使用 Ring0 和 Ring3,Ring0 具有最高权限,能够访问任何资源,Ring3 访问受限,需要陷入(trap)到内核态才能访问特权资源。
栈(stack)从虚拟地址 0xC0000000
往低地址增长。堆(heap)正好相反,从低地址往高地址增长。栈用于函数调用以及存放局部变量等,堆用于动态内存分配,如 C 语言中的 malloc() 函数。栈与操作系统内核之间有一个随机的 offset,堆与读/写段之间也有一个随机的 offset。
内存映射段(Memory Mapping Segment)用于将文件内容映射到内存,用于加载动态链接库,可以通过 mmap() 系统调用实现内存映射。
bss 段放的是未初始化的全局变量和静态变量,默认都初始化为 0,data 段存放的是初始化的全局变量和静态变量。
text 段存放的是程序代码,除了可读还具有可执行权限。
栈及其操作
栈是一种先进后出的结构,包含两种操作:push
和 pop
。
在 IA-32 体系结构中,通常用 ESP
和 EBP
维护一个栈,EBP
指向栈底,ESP
指向栈顶。
IA-32 中 PUSH
指令先减少 ESP
的值,再将源操作数复制到堆中。
如 PUSH EAX
等价于
SUB ESP, 4
MOV [ESP], EAX
POP
指令正好相反,如 POP EBX
等价于
MOV EBX, [ESP]
ADD ESP, 4
栈帧
栈帧(stack frame)用于函数调用,每一次函数调用都会有一个独立的栈帧,包含了返回地址、局部变量、上下文等信息。
high address +--------------------+ <--+ | | | | ...... | | previous frame| | |+--------------------+ <--+ +------> | previous EBP | || +--------------------+ || | saved registers | || +--------------------+ || | local variables | | caller's frame| +--------------------+ || | callee's args | || +--------------------+ || | return address | || +--------------------+ <--++------- | previous EBP | |+--------------------+ | | local variables | |+--------------------+ || | | callee's frame| | || | || | || | |+--------------------+ <--+low address
调用者(Caller) 调用 被调用者,每次调用都会有新的栈帧压栈,所以一般深度优先搜索可以用栈来代替递归,以达到更深的搜索深度。
ESP
和 EBP
只维护当前的栈帧,因此之前的栈帧都要保存下来,栈帧的顶部保存了上一个栈帧的 EBP
指向的位置,函数返回时 EBP
能够恢复到上一个栈帧的 EBP
。
函数调用过程
调用过程
调用者 调用 被调用者 前,先保存返回地址,即下一条指令的地址,用于返回后继续执行,然后进入被调用者函数。
被调用者开辟了新的栈帧,因此需要保存调用者的栈帧,通常使用以下两条指令:
PUSH EBP
MOV EBP, ESP
先把旧的 EBP
入栈,然后让 EBP
指向旧的 EBP
,此时 EBP
已经作为新的栈帧的栈底了。
函数调用时,为了防止寄存器被覆盖,有时需要将寄存器内容也保存到栈中。
参数的传递
IA-32 下,在调用者函数中,参数从右往左入栈。进入被调用者函数时,参数以及局部变量的访问以 EBP
为基址,通过 EBP
加上偏移量访问。
x86-64 下参数传递略有不同。如果函数调用参数少于 7 个,则用寄存器传递参数,参数从左到右依次放入寄存器 RDI
,RSI
,RDX
,RCX
,R8
,R9
。参数超过 7 个时,前 6 个参数同样通过寄存器传参,之后的参数与 32 位一样从右往左压入栈中。
返回过程
返回值保存在 EAX 寄存器中。
返回时先将 EBP
恢复至上一个栈帧的栈底,通常使用以下两条指令:
MOV ESP, EBP
POP EBP
然后将保存的返回地址放入 EIP
寄存器。
最后将保存的参数出栈。
涉及的指令
先将当前 EIP(即下一条指令的地址作为返回地址)压入栈中,然后 EIP 转移到被调用者的入口地址。
从栈顶弹出原来保存的地址至 EIP。
通常将返回时涉及的两条指令用 LEAVE
指令代替,也就是说 LEAVE
等价于上述提到的两条指令:
MOV ESP, EBP
POP EBP
函数的调用约定
上述参数从右往左入栈、返回值存入 EAX
寄存器等不是硬性规定的,而是遵守函数的调用约定(calling convention)。函数的调用约定规定了参数的传递顺序、参数和返回值放置的位置、调用前后设置的工作由调用者完成还是被调用者完成等。上文提到的都是 C 调用约定(cdecl调用约定)。其他的调用约定有 stdcall调用约定、fastcall调用约定、thiscall调用约定等。
一个简单的例子
以一个加法函数为例:
// file: add.c
#include <stdio.h>int add(int a, int b) {int c = a + b;return c;
}int main() {int a = 1, b = 2;int c = add(a, b);printf("%d\n", c);return 0;
}
编译成 32 位程序:
gcc add.c -o add -m32
查看汇编代码(汇编代码可以用 gdb、objdump、IDA 等工具查看,也可以直接用 gcc 编译成汇编代码)。
输入 layout asm
查看汇编代码:
进入 main() 函数后,先保存上一个栈帧的 EBP
(main 函数不是第一个被调用的函数)。
high address +--------------------+ <--+ | | | | ...... | | previous frame| | |+--------------------+ <--+ ESP,EBP ------> | previous EBP | | main()'s frame+--------------------+ <--+low address
然后保存寄存器。
high address +--------------------+ <--+ | | | | ...... | | previous frame| | |+--------------------+ <--+ EBP ------> | previous EBP | |+--------------------+ | main()'s frameESP ------> | saved registers | |+--------------------+ <--+low address
开辟一定的空间保存局部变量。
high address +--------------------+ <--+ | | | | ...... | | previous frame| | |+--------------------+ <--+ EBP ------> | previous EBP | |+--------------------+ || saved registers | |+--------------------+ || ...... | |+--------------------+ | main()'s frame| b=2 | |+--------------------+ || a=1 | |+--------------------+ |ESP ------> | | |+--------------------+ <--+low address
压入 add() 函数的参数。
high address +--------------------+ <--+ | | | | ...... | | previous frame| | |+--------------------+ <--+ EBP ------> | previous EBP | |+--------------------+ || saved registers | |+--------------------+ || ...... | |+--------------------+ || b=2 | | main()'s frame+--------------------+ || a=1 | |+--------------------+ || 1 | |+--------------------+ |ESP ------> | 2 | |+--------------------+ <--+low address
call add() 函数,先保存下一条指令的地址,然后跳转到 add() 函数。
high address +--------------------+ <--+ | | | | ...... | | previous frame| | |+--------------------+ <--+ EBP ------> | previous EBP | |+--------------------+ || saved registers | |+--------------------+ || ...... | |+--------------------+ || b=2 | |+--------------------+ | main()'s frame| a=1 | |+--------------------+ || 1 | |+--------------------+ || 2 | |+--------------------+ |ESP ------> | 0x122b | |+--------------------+ <--+low address
进入 add() 函数,先保存 main() 函数的 EBP
。
high address +--------------------+ <--+ | | | | ...... | | previous frame| | |+--------------------+ <--+ | previous EBP | |+--------------------+ || saved registers | |+--------------------+ || ...... | |+--------------------+ || b=2 | |+--------------------+ | main()'s frame| a=1 | |+--------------------+ || 1 | |+--------------------+ || 2 | |+--------------------+ || 0x122b | |+--------------------+ <--+ESP,EBP ------> | previous EBP | | add()'s frame+--------------------+ <--+low address
开辟空间用以保存局部变量。
high address +--------------------+ <--+ | | | | ...... | | previous frame| | |+--------------------+ <--+ | previous EBP | |+--------------------+ || saved registers | |+--------------------+ || ...... | |+--------------------+ || b=2 | |+--------------------+ | main()'s frame| a=1 | |+--------------------+ || 1 | |+--------------------+ || 2 | |+--------------------+ || 0x122b | |+--------------------+ <--+EBP ------> | previous EBP | |+--------------------+ || ...... | | add()'s frame+--------------------+ |ESP ------> | | |+--------------------+ <--+low address
通过 EBP
加上偏移获取 add() 函数的两个参数,然后求和,结果保存在 EAX
寄存器(EAX
是累加寄存器)。
累加结果放入局部变量 c。
high address +--------------------+ <--+ | | | | ...... | | previous frame| | |+--------------------+ <--+ | previous EBP | |+--------------------+ || saved registers | |+--------------------+ || ...... | |+--------------------+ || b=2 | |+--------------------+ | main()'s frame| a=1 | |+--------------------+ || 1 | |+--------------------+ || 2 | |+--------------------+ || 0x122b | |+--------------------+ <--+EBP ------> | previous EBP | |+--------------------+ || c=3 | |+--------------------+ | add()'s frame| ...... | |+--------------------+ |ESP ------> | | |+--------------------+ <--+low address
将 c 的值放入 EAX
寄存器作为返回值。
然后 add() 函数返回,先执行 LEAVE
指令,恢复 main() 函数的 EBP
。
MOV ESP, EBP
high address +--------------------+ <--+ | | | | ...... | | previous frame| | |+--------------------+ <--+ | previous EBP | |+--------------------+ || saved registers | |+--------------------+ || ...... | |+--------------------+ || b=2 | |+--------------------+ | main()'s frame| a=1 | |+--------------------+ || 1 | |+--------------------+ || 2 | |+--------------------+ || 0x122b | |+--------------------+ <--+ESP,EBP ------> | previous EBP | |+--------------------+ || c=3 | |+--------------------+ | add()'s frame| ...... | |+--------------------+ || | |+--------------------+ <--+low address
POP EBP
high address +--------------------+ <--+ | | | | ...... | | previous frame| | |+--------------------+ <--+ EBP ------> | previous EBP | |+--------------------+ || saved registers | |+--------------------+ || ...... | |+--------------------+ || b=2 | |+--------------------+ | main()'s frame| a=1 | |+--------------------+ || 1 | |+--------------------+ || 2 | |+--------------------+ |ESP ------> | 0x122b | |+--------------------+ <--+| previous EBP | |+--------------------+ || c=3 | |+--------------------+ | add()'s frame| ...... | |+--------------------+ || | |+--------------------+ <--+low address
然后执行 RET
指令,将返回地址赋给 EIP
,从 add() 函数返回至 main() 函数,EIP
的值为 0x122b
,即下一条指令为 main() 函数的 ADD ESP, 0x8
high address +--------------------+ <--+ | | | | ...... | | previous frame| | |+--------------------+ <--+ EBP ------> | previous EBP | |+--------------------+ || saved registers | |+--------------------+ || ...... | |+--------------------+ || b=2 | | main()'s frame+--------------------+ || a=1 | |+--------------------+ || 1 | |+--------------------+ |ESP ------> | 2 | |+--------------------+ <--+low address
执行 ADD ESP, 0x8
,清除 add() 函数的两个参数。
high address +--------------------+ <--+ | | | | ...... | | previous frame| | |+--------------------+ <--+ EBP ------> | previous EBP | |+--------------------+ || saved registers | |+--------------------+ || ...... | | main()'s frame+--------------------+ || b=2 | |+--------------------+ |ESP ------> | a=1 | |+--------------------+ <--+low address
至此 add() 函数的调用完成,可以在 main() 函数继续执行之后的指令了。
系统调用
应用程序无法调用内核函数,需要通过系统调用陷入到内核态,由操作系统执行。
系统调用通过 INT 0x80
中断实现,不同的系统调用有不同的系统调用号,系统调用号需放在 EAX
寄存器中。
系统调用号在 /usr/include/asm/ 中定义。
查看 i386 体系结构下的系统调用号:
陷入到内核态前需要保护现场,将寄存器、程序计数器压入内核栈,然后调用相应的内核函数(系统调用)。内核函数执行完成后,将返回值放入 EAX
寄存器,然后恢复现场,将寄存器、程序计数器从内核栈恢复,
然后恢复到用户态。保护现场和恢复现场均由中断处理程序完成。
系统调用参数传递与函数调用参数传递略有不同,当系统调用参数不超过 6 个时,参数从左到右放到寄存器 EBX
,ECX
,EDX
,ESI
,EDI
,EBP
中,如果参数超过 6 个,所有参数应该放在一块连续的内存区域里(C 结构体),用寄存器 EBX
保存指向该内存区域的指针。
应用程序一般通过 API 去完成系统调用。API 与 系统调用 不同,一个 API 可能调用多个系统调用,不同的 API 也可能调用同一个系统调用。
基本的流程可以概括为:
- 应用程序调用 API
- API 将系统调用号存入
EAX
,然后通过中断调用系统调用 - 中断处理程序保存寄存器至内核栈,陷入内核态,根据系统调用号调用对应的内核函数
- 内核函数完成工作,保存返回值至
EAX
- 中断处理程序从内核栈恢复寄存器,恢复到用户态,返回到 API
- API 将
EAX
返回给应用程序
系统调用的跟踪可用 strace 工具。
参考
CTF Linux pwn快速入门 - 哔哩哔哩
Linux 内核分析 - 网易云课堂
《Operating Systems: Three Easy Pieces》
access 调用 webbrowser_函数调用过程与栈帧结构 - wuli涛涛相关推荐
- 过程(栈帧结构是干货)
[0]写在前面 过程(栈帧结构是干货):本文总结于csapp, 加上自己的理解: [1]栈帧结构 每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息. 过程调用:函数调用另 ...
- C语言的函数调用过程(栈帧的创建与销毁)
从汇编的角度解析函数调用过程 看看下面这个简单函数的调用过程: 1 int Add(int x,int y)2 {3 int sum = 0;4 sum = x + y;5 return sum;6 ...
- 函数调用过程以及栈帧详解
函数的调用是一个过程,那么在函数的调用过程中要开辟栈空间,用来对本次函数的调用中需要的临时变量保存.这块空间叫栈帧.这个过程调用包括将数据和控制从代码的一部分传递到另一部分.过程调用的任务:为过程的局 ...
- C语言编程宏定义的优缺点,C语言重要知识点总结(二)--内存结构、函数调用过程(栈帧)、宏的优缺点以及##和#的使用...
一.内存结构 内存大致可以分为四个部分:代码段,静态存储区,堆,栈. 具体划分如下图所示: 栈:在执行函数时,函数内部局部变量的存储单元都可以在栈上创建,函数执行结束后会自动释放内存.栈内存的分配运算 ...
- 函数调用过程,栈帧的一点理解
栈帧图例一张 寄存器理解 程序寄存器组是唯一能被所有函数共享的资源.虽然某一时刻只有一个函数在执行,但需保证当某个函数调用其他函数时,被调函数不会修改或覆盖主调函数稍后会使用到的寄存器值.因此,IA3 ...
- 函数调用过程中的栈帧结构及其变化
前言:本文旨在从汇编代码的角度出发,分析函数调用过程中栈帧的变化. 栈帧的简单介绍: 当某个函数运行时,机器需要分配一定的内存去进行函数内的各种操作,这个过程中分配的那部分栈称为栈帧.下图描述了栈帧的 ...
- (栈帧和函数调用一)栈帧,函数调用与栈的关系
(栈帧和函数调用一)栈帧,函数调用与栈的关系 一,栈帧的介绍 二,函数调用与栈的关系 三,汇编演示 四,总结 在计算机科学中,栈是一个特殊的容器,用户可以将数据压入栈中(入栈,push),也可以将已经 ...
- java 参数类型不确定_详细解析Java虚拟机的栈帧结构
什么是栈帧? 正如大家所了解的,Java虚拟机的内存区域被划分为程序计数器.虚拟机栈.本地方法栈.堆和方法区.(什么?你还不知道,赶紧去看看<Java虚拟机内存结构及编码实战>)这次要介绍 ...
- java虚拟机栈帧_Java虚拟机,运行时栈帧结构
业余生活要有意义,不要越轨.--华盛顿 引导语 "虚拟机"是一个相对于"物理机"的概念,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器. ...
- 11.JDK8内存模型、本地方法栈、虚拟机栈、栈帧结构(局部变量表、操作数栈、方法出口、虚拟机栈与本地方法栈的关系、寄存器、方法区、堆(Heap)、jvm中的常量池、Metaspace(元空间))
11.JDK8内存模型 11.1.本地方法栈(Native Method Stacks) 11.2.虚拟机栈(Java Virtual Machine Stacks) 11.3.栈帧结构 11.3.1 ...
最新文章
- JAVA基础面试中的几个问题
- 万字图文 | 聊一聊 ReentrantLock 和 AQS 那点事(看完不会你找我)
- vscode-icons插件使用
- oracle表空间名字忘了,忘记Oracle用户名密码、及表空间对应的数据库文件地址
- Feature Extractor[VGG]
- 线性代数-求解地球法线
- 数字电子技术基础-阎石老师版本-学习记录
- ACM算法模板 · 一些常用的算法模板-模板合集(打比赛专用)
- 前端视频播放初探总结,video标签-视频插件jwplayer
- 修改自走棋服务器,多多自走棋:各种服务器区分,玩家关心问题集锦,先锋服更新!...
- SpringCLoud+redis+es高并发项目《九》(Spring Security Oauth2 JWT)
- 使用mysql.help_topic生成序列
- kingsoft的服务器信息,Win10系统kingsoft是什么文件夹?可以删除吗?
- java实现mysql的导入导出_Java实现mysql导入导出Excel
- 计算机组成:真正理解“乘法器”和“除法器”
- CentOS配置本地Yum源、阿里云Yum源、163Yum源、并配置Yum源的优先级
- 单片机应用程序开发QY-JXSY51
- FileZilla使用方法
- 火狐浏览器CSS兼容的解决方法~
- Shell脚本使用jq解析json
热门文章
- 安卓Push Rejected解决
- 大数据分页实现与性能优化【转】
- 水星路由器设置显示服务器,新款水星无线路由器设置_新版水星(MERCURY)路由器设置教程-192路由网...
- 计算机管理无法格式化硬盘,sd无法格式化怎么解决_电脑无法格式化sd卡怎么办-win7之家...
- 红旗 Linux 5.0 正式版下载地址
- HttpResponse 417
- 怎么将两台计算机ping接通,小编教你手把手教你一根网线连接两台电脑实现数据传送...
- 信息安全-安全专业名称|CVE|RCE|POC|VUL|0DAY
- 中国城市交通管理体制改革初探
- 英语四级和计算机一级算多少学分,英语四级成绩怎么算分 多少分合格