*本网站图片外链出现错误(懒得修),如需要图文并茂请移步我的blog:una.cetacis.dev *
第十章 内存
10.1 内存布局
- 内核
- 用户空间
- 栈(stack):用于维护函数调用的上下文。位于最高地址处分配,MB大小。
- 堆(heap):容纳应用程序动态分配的内存区域,malloc或new分配的内存来自堆。在栈下方,几十到百兆。
- 可执行文件映象:装载器装载可执行文件的内存读取/映射到这里
- 保留区:内存中受保护禁止访问的内存区域总称
- 动态链接库映射区:映射装载的动态链接库
10.2 栈与调用惯例
-
stack 栈
先进后出。i386下,栈顶指针是esp。
-
栈保存了了一个函数调用所需要维护的信息,被称为stack frame或者activate record。
因为寄存器在函数执行前和执行后的内容应该是相同的,所以设置栈来保存他们的内容,在函数结束后再pop出去。
stack frame包括如下内容:
- 函数的返回地址和参数
- 临时变量:非静态局部变量、编译器自动生成的其他临时变量
- 保存的上下文:函数调用前后需要保持不变的寄存区。
-
ebp寄存器
ebp寄存器又称为frame pointer(帧指针)
-
函数调用过程
- 将参数压栈,如果有参数未入栈,则用某些寄存器传递
- 当前指令的下一条指令入栈(返回地址)
- 跳转函数体
- Push ebp ebo入栈(old ebp)
- mov ebp,esp: ebp=esp (ebp指向esp,即ebp指向栈顶)
- 【可选】sub esp,xxx:在栈上分配xxx字节的临时空间
- 【可选】push xxxx: 如有必要,保存名为xxx寄存器(可重复多个)
将ebppush是为了函数返回时能回复从前的ebp。之所以保存寄存器在于编译器要求某些寄存器调用前后保持不变,函数就在调用开始将寄存器值压如栈,结束取出
- 【可选】pop xxx: 如之前push,则恢复保存过的寄存器(可重复多个)
- mov esp,ebp: 恢复esp同时回收局部变量空间
- pop ebp:从栈中恢复保存的ebp值
- ret:从栈中取得返回地址(下一条指令地址),并跳转。
-
分析具体函数
foo函数
int foo(){ return 123; }
-
总结基本格式
regn 指的是n个寄存器
-
调用惯例
内容:
-
函数参数的传递顺序和方式
-
栈的维护方式
-
名字修饰策略
-
10.3 堆内存的管理
-
什么是堆
栈上数据在函数返回的时候会被释放掉,无法将数据传递到函数外部,全局变量没办法动态地生成,只有在编译的时候定义,堆在此时是好的选择。
堆是一块内存空间,占据虚拟空间的绝大部分,程序可以请求一块连续的内存自由地使用,在程序主动放弃前均有效。
int main(){ char *p = (char*)malloc(1000); free(p); }
堆空间一般由运行库管理,运行库使用堆的分配算法,杜绝地址的冲突。
-
Linux进程堆管理
地址空间中除可执行文件、共享库等之外,剩余未分配空间均可用作堆空间。Linux提供了两种系统调用:
brk() 和 mmap()
brk()设置数据段(数据段和bbs)的结束地址
mmap()向系统申请一段虚拟地址空间,可映射到某个文件,当不映射到文件时,称这块空间为匿名(Anonymous)空间。作为堆空间。
Prot/flags 设置空间权限和映射类型、最后两个用于文件映射指定文件描述符和文件偏移。
mmap()申请大小/地址为系统页整数倍,字节小请求使用mmap浪费空间
- 分配算法
- 空闲链表法
- 位图
- 对象池
十一章 运行库
-
入口函数
显然,函数不是从main开始运行的,当进入main函数时,已经有某些代码准备好main函数执行所需要的环境。并负责调用main函数。在main返回后,它会记录main函数的返回值,调用atexit注册函数,结束进程。
入口函数即是运行这些代码的函数,程序入口是一个程序初始化和结束的部分,是运行库的一部分。
典型程序运行步骤:
- 操作系统创建进程,控制权交予程序入口,入口往往是运行库的某入口函数。
- 入口函数对运行库、程序运行环境初始化,包括堆、I/O、线程等
- 初始化完成后,调用main。
- main完毕后,返回入口函数,入口函数清理,包括全局变量析构、堆销毁、关闭I/O等,进行系统调用结束进程。
入口函数有两种:glibc和MSVC的入口函数实现
-
glibc入口函数
选取glibc最简单的静态作用于可执行文件
(1)glibc入口为_start, _strat由汇编实现,与平台无关。
最终调用了名为_libc_start_main的函数。开头7个压栈指令用于给函数传递参数。
-
xor %ebp, %ebp 是将ebp清零,xor是操作数异或的意思,结果存在第一个操作数里。ebp设为零的目的是表明当前是程序的最外层函数。
-
pop %esi
mov %esp, %ecx
在调用_start前,装载器会把用户的参数和环境变量压入栈,实际上栈顶元素是argc,其下是argv和环境变量的数组。pop前是虚线栈顶,pop后是实线栈顶。pop %esi是将argc存入了esi,mov %esp, %ecx 将栈顶地址(argv起始地址)传递给ecx。
环境变量:存在于系统中的一些公用数据,程序均可访问,例如系统搜索路径,当前os版本等。环境变量格式key = value 的字符串。c可用getenv获取。
(2)实际执行代码是_libc_start_main,以下是 _libc_start_main的函数头部
一共声明了七个参数。
-
main由第一个参数传入,接着是argc,argv(ubp_av,包含环境变量表)。
-
外部还要穿入三个函数指针 init:main调用前的初始化工作。fini:main结束后的收尾工作。rtld_fini:和动态加载有关的收尾工作,rtld是runtime loader的缩写。
-
stack_end表明了栈底地址
(3)实际执行代码是_libc_start_main
下图是我们从_start源代码分析得到的栈布局,让 _environ指向环境变量数组
过滤后得到接下来重要函数
_cxa _atexit是glibc的内部函数,等同atexit,用于将参数指定函数在main之后调用。所以fini和
rtld _fin均是在main结束后调用
接下来为_libc_start_main的末尾
最后,看看exit的实现
-
-
运行库与I/O
(1)I/O全称 Input/Output,输入输出。
对于计算机来说,I/O代表了计算机与外界的交互。
对于程序来说,I/O指代了程序与外界的交互,包括文件、管道、网络、命令行、信号。
Linux/windows将各种具体输入输出概念(设备、磁盘文件、命令行)称为文件,文件是一个广义的概念。
(2)句柄(handle)
定义:FILE结构的指针可用于文件的操作,使用fopen/fwrite来对这个指针操作,进而作用于文件。在操作系统层面,文件操作也有类似于FILE的概念,在linux里叫做文件描述符(File Descriptor),在windows中,称为句柄(handle)
打开文件表是一个指针数组,每一个元素指向一个内核的打开文件对象(内核对象)。fd是这个表的下表, 内核指针p为这个表的初试地址。用户打开一个文件,内核会生成一个打开文件对象(并在打开文件表中生成指针,指向这个打开文件对象),并且返回这个指针的下表fd。这个表在内核,用户不能直接访问到(因为不知道p),因此只能通过系统提供的函数(fwrite/fopen)来操作,保证了安全性。
C中的FILE其实是与fd有一对一的关系,可以借FILE访问文件。
Windows中的句柄和fd大同小异。不过他不是打开文件表的下标,而是下标经过线性变化的结果。
11.2 C/C++运行库
-
CRT(C Runtime Library)
功能:
- 启动退出:入口函数和入口函数依赖的其他函数
- 标准函数:C语言标准规定的C语言标准库所拥有的的函数 C89C99
- I/O:I/O功能的封装和实现
- 堆:堆的封装、实现、初始化
- 语言实现:语言的特殊功能实现
- 调试
-
C的标准库
- C标准库很轻量。如:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ntmh3twq-1582117545599)(http://47.101.139.155:23333/images/2020/02/13/Screen-Shot-2020-02-13-at-9.34.45-PM.png)]
-
glibc和MSVC CRT
C语言运行库从某种程度讲是C语言程序和不同操作系统平台之间的抽象层,将不同操作系统API抽象成相同库函数。
但是有的操作系统功能没有对应的标准CRT,所以我们不得不让C语言运行库直接调用操作系统API和其他库。Linux和windows平台下的两个主要C分别为glibc(GNU C Library)和MSVCRT(Microsoft Visual C Run-time)
glibc和Linux的关系:glibc原为GNU旗下的C标准库,GNU原定内核是HUR(微内核构架系统),但是Hurd开发缓慢,Linux因实用性风靡,代替Hurd称为GNU操作系统内核,因此glibc变成了Linux平台的C标准库。
glibc由头文件(stdio.h, stdlib.h,位于/usr/include)和二进制文件部分(C语言标准库,静态动态两个版本,动态位于/lib/libc.so.6,静态位于/usr/lib/libc.a)。此外,还有一些辅助运行的库。比如/usr/lib/crt1.o,/usr/lib/crti.o,/usr/lib/crtn.o。
第十二章 系统调用
Linux内核版本2.6.19总共319个系统调用。可以使用read系统调用实现用户输入。不过绕过了glibc,则glibc的文件机制没有了。
12.1 系统调用的弊端
-
调用接口过于原始,没有进行包装,使用不便。
-
操作系统之间的调用不兼容。(通过添加系统调用和程序之间的运行库解决)
比如 C中fread库函数,Windows调用ReadFile这个API,Linux调用read这个系统调用。所以,都可以用CRT的fread来读。