程序员的自我修养(三)内存分布,运行库与系统调用

*本网站图片外链出现错误(懒得修),如需要图文并茂请移步我的blog:una.cetacis.dev *

第十章 内存

10.1 内存布局
  • 内核
  • 用户空间
    • 栈(stack):用于维护函数调用的上下文。位于最高地址处分配,MB大小。
    • 堆(heap):容纳应用程序动态分配的内存区域,malloc或new分配的内存来自堆。在栈下方,几十到百兆。
    • 可执行文件映象:装载器装载可执行文件的内存读取/映射到这里
    • 保留区:内存中受保护禁止访问的内存区域总称
    • 动态链接库映射区:映射装载的动态链接库
10.2 栈与调用惯例
  1. stack 栈

    先进后出。i386下,栈顶指针是esp。

  2. 栈保存了了一个函数调用所需要维护的信息,被称为stack frame或者activate record。

    因为寄存器在函数执行前和执行后的内容应该是相同的,所以设置栈来保存他们的内容,在函数结束后再pop出去。

    stack frame包括如下内容:

    • 函数的返回地址和参数
    • 临时变量:非静态局部变量、编译器自动生成的其他临时变量
    • 保存的上下文:函数调用前后需要保持不变的寄存区。
  3. ebp寄存器

    ebp寄存器又称为frame pointer(帧指针)

  4. 函数调用过程

    • 将参数压栈,如果有参数未入栈,则用某些寄存器传递
    • 当前指令的下一条指令入栈(返回地址)
    • 跳转函数体
    • 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:从栈中取得返回地址(下一条指令地址),并跳转。
  5. 分析具体函数

    foo函数

    int foo(){
      return 123;
    }
    
  6. 总结基本格式

regn 指的是n个寄存器

  1. 调用惯例

    内容:

    • 函数参数的传递顺序和方式

    • 栈的维护方式

    • 名字修饰策略

10.3 堆内存的管理
  1. 什么是堆

    栈上数据在函数返回的时候会被释放掉,无法将数据传递到函数外部,全局变量没办法动态地生成,只有在编译的时候定义,堆在此时是好的选择。

    堆是一块内存空间,占据虚拟空间的绝大部分,程序可以请求一块连续的内存自由地使用,在程序主动放弃前均有效。

    int main(){
      char *p = (char*)malloc(1000);
      free(p);
    }
    

    堆空间一般由运行库管理,运行库使用堆的分配算法,杜绝地址的冲突。

  2. Linux进程堆管理

    地址空间中除可执行文件、共享库等之外,剩余未分配空间均可用作堆空间。Linux提供了两种系统调用:

    brk() 和 mmap()

    brk()设置数据段(数据段和bbs)的结束地址

    mmap()向系统申请一段虚拟地址空间,可映射到某个文件,当不映射到文件时,称这块空间为匿名(Anonymous)空间。作为堆空间。

Prot/flags 设置空间权限和映射类型、最后两个用于文件映射指定文件描述符和文件偏移。

mmap()申请大小/地址为系统页整数倍,字节小请求使用mmap浪费空间

  1. 分配算法
    • 空闲链表法
    • 位图
    • 对象池

十一章 运行库

  1. 入口函数

    显然,函数不是从main开始运行的,当进入main函数时,已经有某些代码准备好main函数执行所需要的环境。并负责调用main函数。在main返回后,它会记录main函数的返回值,调用atexit注册函数,结束进程。

    入口函数即是运行这些代码的函数,程序入口是一个程序初始化和结束的部分,是运行库的一部分。

    典型程序运行步骤:

    • 操作系统创建进程,控制权交予程序入口,入口往往是运行库的某入口函数。
    • 入口函数对运行库、程序运行环境初始化,包括堆、I/O、线程等
    • 初始化完成后,调用main。
    • main完毕后,返回入口函数,入口函数清理,包括全局变量析构、堆销毁、关闭I/O等,进行系统调用结束进程。

    入口函数有两种:glibc和MSVC的入口函数实现

  2. 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的实现

  3. 运行库与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++运行库
  1. CRT(C Runtime Library)

    功能:

    • 启动退出:入口函数和入口函数依赖的其他函数
    • 标准函数:C语言标准规定的C语言标准库所拥有的的函数 C89C99
    • I/O:I/O功能的封装和实现
    • 堆:堆的封装、实现、初始化
    • 语言实现:语言的特殊功能实现
    • 调试
  2. 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来读。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值