执行helloworld时系统内部到底发生了什么


0x00 简述

经常被老师灵魂拷问,知道执行helloworld时系统内部到底发生了什么么。

终于有时间把相关的东西看了一些,写了点东西做总结。

好像条理想并不是那么强,也把好多个概念杂糅起来了,就当做抛砖引玉吧。

以此程序为例。

// position: /home/test/hello/hello.c
#include <stdio.h>

int main(){
    printf("hello world!\n");
    return 0;
}



其中,按照每一节解释一个命令,命令如下

cd /home/test/hello   #0x01
g++ hello.c -o a.out  #0x02
./a.out			      #0x04

注1:这里将可执行文件命名为a.out是因为书上的图例是a.out,懒的改图。。

注2:0x03写了执行文件时进程那边有啥变化。

注3:附加内容是扩展,可以不看。


0x01 从输入命令到找到文件
cd /home/test/hello

每个进程都由一个u区,u区包含一个指向当前目录索引节点的指针。其中,系统根索引节点被存储在一个全局变量中。

当一个进程向打开文件"/home/test/hello",当内核开始分析这个文件名时,它遇到"/",并获得了系统根索引节点。把根作为它的当前工作索引节点后,内核读入字符串”home"。在核对了该进程有必要的搜索权限后,就遍历工作索引节点,直到找到名为“home”的文件,然后释放“/"工作节点,为“home”分配索引节点,并将它作为工作节点。以此类推,直到找到最终的“hello”文件夹。

注:分配索引节点的意思为,把索引节点从硬盘读到内存。


0x02 程序编译,连接

执行如下命令开始编译

unix> g++ hello.c -o a.out

系统会调用G++驱动程序,里面包含语言预处理器,编译器,汇编器和链接器。

预处理器会把程序hello.c翻译成一个ASCII码的中间文件hello.i。

C编译器会把hello.i翻译成一个ASCII汇编语言文件hello.s。

汇编器将hello.s翻译成一个可重定位目标文件hello.o。

然后调用链接器,将多个可重定位目标文件(如果有的话,本例中没有)链接成一个可执行文件a.out

至此,编译链接完成,生成一个a.out文件。


附加内容;

​ 预处理器和编译器内容还没看,先说下链接器。

​ 每一个c文件都会生成一个可重定位目标文件。文件格式如下所示。

在这里插入图片描述

其中

ELF头:以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。

text:以编译程序的机器代码。

rodata:只读数据。

data:已初始化的全局C变量。

bss:未初始化的全局C变量。

symtab:符号变,即使不加-g选项也会生成,不包含局部变量的条目。

rel.text:代码重定位信息。

rel.data:数据的重定向数据。

debug:-g选项会得到这张表。包含局部变量和类型定义。

line:C源程序中的行号和text节中机器指令之间的映射。

strtab:字符串表。

注:static属性的本地过程变量是不在栈中管理的。他会初始化在data或bss中。并且编译器把初始化为0的static变量放在bss中而不是data中。

注2:链接器如何解析多重定义的全局符号。

函数和已初始化的全局变量是强符号,未初始化的全局变量是弱符号。

规则一:不允许多个强符号。

规则二:如果有一个强符号和多个弱符号,那么选择强符号。

规则三:如果有多个弱符号,则从其中任意选择一个。


链接静态库

链接静态库的时候,会把被程序引用的目标模的相应信息块拷贝到当前可重定位目标文件当中。这样虽然执行会正常,但是增加了可执行文件的大小。

静态库是在链接的时候完成链接的。在符号解析的阶段,链接器从左到右按照他们在编译器驱动程序命令行出现的顺序来扫描可冲定位目标文件和存档文件(静态库)。

因为是从左到右扫描,所以顺序就显得十分重要。

举例,A文件调用了B文件中定义的函数。命令为先A后B

unix> g++ -static A B

大概过程为:当扫描到第一个文件A的时候,如果有未定义的函数/变量,则默认在其他文件中定义,将未定义的函数/变量信息保存在一个表中,扫描到第二个文件B的时候对表中信息进行搜索,如果搜索到,则从表中去除该条信息。到最后如果表中为空,则正常输出。否则报错"undefine reference"。

unix> g++ -static B A

那么如果顺序出错,则第一个文件中B没有未定义的信息,则表中为空,扫描到文件A的时候发现A中有未定义的信息,但是B已经扫描过了,不会再次扫描,就会报错。


链接动态库

动态库是在程序加载的时候完成链接的。

动态库是所有程序共享一份代码,所以不会像静态库那样增加内存占用。只会占用一份。

在生成可执行文件的过程中,链接器只拷贝了一些重定位和符号表信息,它们使得运行时可以解析对动态库中代码和数据的引用。

加载器完成这部分重定位信息的重定位工作,从这个时候开始,共享库(动态库)的位置就固定了,并且在程序执行过程中不会改变。


0x03 打开文件

先介绍三个概念:文件描述符表,打开文件表,i-node(v-node)。

文件描述符表:每个进程一个,负责记录这个进程打开的文件。它的表项是由进程打开的文件描述符来索引的。每个打开的描述符表项指向打开文件表的一个表项。

打开文件表:所有的进程共享这张表。每个文件表的表项组成包括有当前的文件位置,引用计数(即当前指向该表项的描述符表项数),以及一个指向v-node表中对应表项的指针,直到他的引用计数为0。

v-node表:同文件表一样,所有进程共享这张v-node表。每个表项包含stat结构中的大多数信息。包括st_mode和st_size成员。


在这里插入图片描述


注:stat结构如下图所示,其中st_mode编码了文件访问许可位,st_size包含了文件的字节数大小。

在这里插入图片描述

当打开文件的时候,如果之前没有打开过此文件,那么三张表中都会增加一项。


附加内容

fork时会发生什么
在这里插入图片描述
描述符表会复制一张父进程的下来,指向同一个打开文件表项。



0x04 加载可执行目标文件
unix> ./a.out

因为p不是一个内置的外壳(shell)命令,所以外壳会认为p是一个可执行目标文件,通过调用某个驻留在存储器中成为加载器(loader)的操作系统代码来运行它。任何Unix程序都可以通过调用execve函数来调用加载器。加载器将可执行目标文件中的代码和数据从磁盘拷贝到存储器中,然后通过跳转到程序的第一条执行或入口(entry point)来运行该程序。

在这里插入图片描述

当加载器运行时,它创建如上图所示的存储器映像。在可执行文件中段头部表的直到下,加载器将可执行文件的相关内容拷贝到代码和数据段。接下来,加载器跳转到程序的入口点,也就是符号_start的地址。在_start地址处的启动代码(startup code)是在目标文件ctrl.o文件中定义的,对所有的C程序都是一样的。下图展示了启动代码中具体的调用序列。在从.text和.init节中调用了初始化例程后,启动代码调用atexit例程,这个程序附加了一系列在应用程序正常终止时应该调用的程序。

接着,启动代码调用应用程序的main函数,他会开始执行C程序,在应用程序返回之后,调用_exit函数,它将控制权返回给操作系统。exit函数运行atexit注册的函数,然后通过调用_exit将控制返回给操作系统。


在这里插入图片描述

注:处了一些头部信息,在加载过程中没有任何从磁盘到存储器的数据拷贝。直到CPU引用了一个被映射的虚拟页才会进行拷贝。此时,操作系统利用他的页面调度机制自动将页面从磁盘传送到存储器。


0x05 执行文件

如0x03中所说,加载可执行文件的时候是不进行磁盘拷贝的,只有当CPU引用了一个被映射的虚拟页的时候才进行拷贝。拷贝的流程如下图所示。


在这里插入图片描述

MMU:(Memory Management Unit) 存储器管理单元,功能是利用存放在主存中的查询表来动态翻译虚拟地址,其实就是主存和CPU之间的缓冲。

PTE:(Page Table Entry) 页表条目。页表中的一行。

把数据扔到内存后,CPU会用程序的第一条指令地址,去内存中取第一条指令的数据。

  1. 处理器生成一个虚拟地址,并把它传送给MMU。
  2. MMU生成PTE地址,并从高速缓存/主存请求得到它。
  3. 高速缓存/主存向MMU返回PTE。
  4. MMU构造物理地址,并把它传送给高速缓存/主存。
  5. 高速缓存/主存返回所请求的数据字给处理器。

页面命中完全是由硬件来处理的,与之不同的是,处理缺页要求硬件和操作系统内核协作完成。

前三步是一样的

  1. PTE中的有效位是0,所以MMU出发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序。
  2. 缺页处理程序确定出物理存储器中的牺牲页,如果这个页面已经被修改了,则把它换出磁盘。
  3. 缺页处理程序页面调入新的页面,并更新存储器中的PTE。
  4. 缺页处理程序返回到原来的进程,再次执行导致缺页的执行。CPU将引起缺页的虚拟地址重新发送给MMU,因为虚拟页面现在缓存在物理存储器中,所以就会命中,将数据返回给处理器。

在这里插入图片描述

至此,CPU拿到了数据,依据本次例子程序,CPU发现是一个输出,便把数据“hello world!”通过系统总线-IO桥-IO总线传输给摄外壳,外壳显示在屏幕上。


注1:事实上,在片上还有一个结构为 翻译后备缓冲器 TLB(Translation Lookaside Buffer)。它负责缓冲一部分PTE,在MMU查询的时候,先向TLB查询,TLB未命中的情况下才执行以上步骤。
注2:其中,在MMU从虚拟地址翻译成物理地址的过程就是上课学过的。好像是计算机体系结构课。不难就不解释了。

在这里插入图片描述


注:3:关于高速缓存的问题,下图是存储器的层次结构图。


在这里插入图片描述


高速缓存关于读的操作非常简单。首先,在高速缓存中查找所需字w的拷贝。如果命中,立即返回字w给CPU。如果不命中,存存储器层次结构中较低层次中取出包含字w的块,将整个块存储到某个高速缓存行汇总,返回返回字w。



0x06 参考书籍

深入理解计算机新系统(原书第二版)
Unix操作系统设计

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值