1 从源程序到执行

1.1 大致流程

demo.c -> demo.i -> demo.s -> demo.o -> demo.exe

image.png
image.png
第四章: 第一个程序 - 图3

1.2 Procedure Call

image.png

1.2.1 栈帧

  1. int funcP(int a,int b,int c){
  2. funcQ(1,2,3,4,5,6,7,8)
  3. }
  4. int funcQ(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8){
  5. }

P函数调用Q函数的时候的内存状况

image.png

1.2.2 转移控制

P调用Q时,P在自己的栈帧顶部使用push指令记录下调用函数的那一行代码的下一行的地址,也叫返回地址,随后设置PC指针指向函数Q的起始代码, CPUP函数的控制权移交给Q Q调用完成后,执行ret指令, Q的栈帧被释放,此时栈顶元素变为Q执行前P压入栈时的返回地址,然后ret指令执行两步: pop ss:sp, 将栈顶元素拷贝到PC中, CPU夺回对P函数的控制权

1.2.3 参数传递

固定数量的参数传递

image.png

如果一个函数有大于6个整形参数,超出6个的部分必须通过栈类传递。 假设P调用过程Q, 如果参数个数n>6, 则前六个参数依次放在%rdi,%rsi,%rdx,%rcx,%r8,%r9中,超出6个的参数分配在P的栈帧上,P通过call调用Q之后, 参数7位于%rsp+8处。 通过栈传递参数的时候,所有数据大小都向8的倍数对齐。 参数到位以后,程序可以执行call指令将控制转移到过程Q了。过程Q可以通过寄存器访问前六个参数,通过P的栈帧访问剩下的参数.

参数构造区

image.png
image.png

1.3 链接器

1.3.1 概念

image.png
image.png

1.3.2 目标文件

可重定位目标文件: 后缀名为.o的文件: 如example.o, 本质是二进制代码(代码段)和数据(数据段), 可以和其他可重定位目标文件联合起来 可执行目标文件: 包含二进制代码和数据,可以直接被loader加载进内存 共享目标文件: 主要是一些动态库,在动态链接中使用的较多,可以在加载或者运行时被动态加载进内存并被linker链接形成可执行文件

可重定位目标文件

image.png

.text: 代码段 .rodata: 只读代码区,比如格式化字符串或者switch table .data: 数据段, 包括已初始化的全局和静态C变量 .bss:未初始化的全局和静态C变量, 以及被初始化为0的全局或静态变量. 这个段不占有内存,只是一个占位符。在运行时,在内存中分配这些变量 .symtab:符号表,存放一些需要引用外部和可以被外部引用的函数和全局变量信息 .rel.text: 代码中调用外部函数或者引用外部全局变量指令都需要修改 .rel.data: 任何以初始化的全局变量,如果它的初始值是一个外部全局变量地址或者外部定义的函数的地址,都需要被修改。 .debug .line .strtab不做重点

image.png

1.3.3 符号决议

https://segmentfault.com/a/1190000016433829

符号表

编译器在编译过程中遇到外部定义的全局变量或函数时,只要编译器能找到相应的变量声明就会在心里默念“all is well, all is well(一切顺利)“,从这里可以看出编译器的要求还是很低的,至于所使用变量的定义编译器是不会费力去四处搜索,而是愉快的继续接下来的编译。注意,这里再次强调一下,编译器在遇到外部定义的全局变量或者函数时只要能在当前文件找到其声明,编译器就认为编译正确。而寻找使用变量定义的这项任务就被留给了链接器。链接器的其中一项任务就是要确定所使用的变量要有其唯一的定义。虽然编译器给链接器留了一项任务,但为了让链接器工作的轻松一点编译器还是多做了一点工作的,这部分工作就是符号表(Symbol table)符号表**(Symbol table)**是由汇编器**Assembler**创建的 image.png image.png

全局与局部,内部和外部,静态与非静态符号

image.png

符号表例子

image.png
image.png

本质上整个符号表只是想表达两件事:

  • 我能提供给其它文件使用的符号
  • 我需要其它文件提供给我使用的符号

符号表练习

image.png
image.png

符号决议过程

符号决议原则

image.png

例1

image.png

例2

image.png

例3

image.png
image.png

例4: 两个弱定义

image.png

1.3.4 库与可执行文件

https://segmentfault.com/a/1190000016433897

静态链接的可执行文件

本质上是不同目标文件的的按段拼接

image.png

从上图中我们可以看到可执行文件的特点:

  • 可执行文件和目标文件一样,也是由代码段和数据段组成。
  • 每个目标文件中的数据段都合并到了可执行文件的数据段,每个目标文件当中的代码段都合并到了可执行文件的代码段。
  • 目标文件当中的符号表并没有合并到可执行文件当中,因为可执行文件不需要这些字段。

可执行文件和目标文件没有什么本质的不同,可执行文件区别于目标文件的地方在于,可执行文件有一个入口函数,这个函数也就是我们在C语言当中定义的main函数,main函数在执行过程中会用到所有可执行文件当中的代码和数据。而这个main函数是被谁调用执行的呢,答案就是操作系统(Operating System),这也是后面文章当中要重点介绍的内容。

静态链接是使用库的最简单最直观的形式, 从静态链接生成可执行文件的过程中可以看到,静态链接会将用到的目标文件直接合并到可执行文件当中,想象一下,如果有这样的一种静态库,几乎所有的程序都要使用到,也就是说,生成的所有可执行文件当中都有一份一模一样的代码和数据,这将是对硬盘和内存的极大浪费,假设一个静态库为2M,那么500个可执行文件就有1G的数据是重复的。如何解决这个问题呢,答案就是使用动态库。

动态链接的可执行文件

加载时链接

加载就是将程序从磁盘加载进内存准备运行的过程 加载时链接会在程序加载进内存,运行之前根据链接的目标文件中的symbol table进行符号决议,进行动态链接

运行时链接

相当于直到某个程序语句被执行的时候才会去进行符号决议

  • 为了和加载时动态链接作比对,我们继续使用上一小节当中读书的例子,加载时动态链接就好比在开始准备读一本书之前,将该书中所有引用到的资料文献找齐全。
  • 而运行时动态链接则不需要这个过程,运行时动态链接就好比直接拿起一本书开始看,看到有引用的参考文献时再去找该资料,找到后查看该文献然后继续读我们的书。从这个例子当中运行时动态链接更像是我们平时读书时的样子。

对比

:::success 通过这个过程也可以清楚的看到静态库和动态库的区别,使用动态库的可执行文件当中仅仅保留相应信息,动态库的链接过程被推迟到了程序启动加载时。

动态链接的可执行文件中包含.data, .text, dynamic, 和GOT

  • 解决了静态链接下磁盘浪费问题,
  • 修改动态库代码不用重新编译客户端代码,
  • 动态链接的这种特性可以用于扩展程序能力(插件),
  • 使用动态链接的程序在性能上要稍弱于静态链接,这时因为对于加载时动态链接,这无疑会减慢程序都启动速度,而对于运行时链接,当首次调用到动态库的函数时,程序会被暂停,当链接过程结束后才可以继续进行。且动态库中的代码是地址无关代码(Position-Idependent Code,PIC),之所以动态库中的代码是地址无关代码是因为动态库又被成为共享库,所有的程序都可以调用动态库中的代码,因此在使用动态库中的代码时程序要多做一些工作,
  • 动态库的一个优点其实也是它的缺点,即动态链接下的可执行文件不可以被独立运行

静态链接的可执行文件中仅包含.data,.text

  • 静态链接是最古老也是最简单的链接技术。静态链接都最大优点就是使用简单,编译好的可执行文件是完备的,即静态链接下的可执行文件不需要依赖任何其它的库,因为静态链接下,链接器将所有依赖的代码和数据都写入到了最终的可执行文件当中,这就消除了动态链接下的库依赖问题,没有了库都依赖问题就意味着程序都安装部署都得到了极大都简化。极大的简化了系统部署以及升级。笔者之前所在的某电商广告后端系统就完全使用静态链接来简化部署升级。
  • 而静态库的缺点相信大家都已经清楚了,那就是静态链接会导致可执行文件过大,且多个程序静态链接同一个静态库的话会导致磁盘浪费的问题。 :::

1.3.5 重定位

https://segmentfault.com/a/1190000016433947

2 程序执行过程

2.1 DOS下的可执行文件的执行

image.png

问题

image.png
image.png
image.png

2.2 程序执行过程的跟踪d

Debug加载可执行文件进内存

image.png

加载过程与程序存放位置

image.png
image.png

3 实验

image.png
image.png
image.png