本文对hello.c这个程序编写完成后在Linux下运行的“程序人生”进行了描述。在这个过程中,我们先后分别使用C预处理器、C编译器、汇编器得到了hello.i、hello.s、hello.o文件,最后利用链接器生成了可执行文件hello。 之后,我们探索了计算机系统对hello的进程管理、存储管理。通过对hello的“程序人生”的探索,我们也能够从程序员的角度,对计算机系统有更深刻的理解。
关键词:hello程序;编译;汇编;链接;进程管理
目 录
第1章 概述................................................................................... - 4 -
1.1 Hello简介............................................................................ - 4 -
1.2 环境与工具........................................................................... - 4 -
1.3 中间结果............................................................................... - 4 -
1.4 本章小结............................................................................... - 4 -
第2章 预处理............................................................................... - 5 -
2.1 预处理的概念与作用........................................................... - 5 -
2.2在Ubuntu下预处理的命令................................................ - 5 -
2.3 Hello的预处理结果解析.................................................... - 5 -
2.4 本章小结............................................................................... - 5 -
第3章 编译................................................................................... - 6 -
3.1 编译的概念与作用............................................................... - 6 -
3.2 在Ubuntu下编译的命令.................................................... - 6 -
3.3 Hello的编译结果解析........................................................ - 6 -
3.4 本章小结............................................................................... - 6 -
第4章 汇编................................................................................... - 7 -
4.1 汇编的概念与作用............................................................... - 7 -
4.2 在Ubuntu下汇编的命令.................................................... - 7 -
4.3 可重定位目标elf格式........................................................ - 7 -
4.4 Hello.o的结果解析............................................................. - 7 -
4.5 本章小结............................................................................... - 7 -
第5章 链接................................................................................... - 8 -
5.1 链接的概念与作用............................................................... - 8 -
5.2 在Ubuntu下链接的命令.................................................... - 8 -
5.3 可执行目标文件hello的格式........................................... - 8 -
5.4 hello的虚拟地址空间......................................................... - 8 -
5.5 链接的重定位过程分析....................................................... - 8 -
5.6 hello的执行流程................................................................. - 8 -
5.7 Hello的动态链接分析........................................................ - 8 -
5.8 本章小结............................................................................... - 9 -
第6章 hello进程管理.......................................................... - 10 -
6.1 进程的概念与作用............................................................. - 10 -
6.2 简述壳Shell-bash的作用与处理流程........................... - 10 -
6.3 Hello的fork进程创建过程............................................ - 10 -
6.4 Hello的execve过程........................................................ - 10 -
6.5 Hello的进程执行.............................................................. - 10 -
6.6 hello的异常与信号处理................................................... - 10 -
6.7本章小结.............................................................................. - 10 -
第7章 hello的存储管理...................................................... - 11 -
7.1 hello的存储器地址空间................................................... - 11 -
7.2 Intel逻辑地址到线性地址的变换-段式管理................... - 11 -
7.3 Hello的线性地址到物理地址的变换-页式管理............. - 11 -
7.4 TLB与四级页表支持下的VA到PA的变换.................... - 11 -
7.5 三级Cache支持下的物理内存访问................................ - 11 -
7.6 hello进程fork时的内存映射......................................... - 11 -
7.7 hello进程execve时的内存映射..................................... - 11 -
7.8 缺页故障与缺页中断处理................................................. - 11 -
7.9动态存储分配管理.............................................................. - 11 -
7.10本章小结............................................................................ - 12 -
第8章 hello的IO管理....................................................... - 13 -
8.1 Linux的IO设备管理方法................................................. - 13 -
8.2 简述Unix IO接口及其函数.............................................. - 13 -
8.3 printf的实现分析.............................................................. - 13 -
8.4 getchar的实现分析.......................................................... - 13 -
8.5本章小结.............................................................................. - 13 -
参考文献....................................................................................... - 16 -
第1章 概述
1.1 Hello简介
Hello的P2P:首先程序员编写程序,命名为hello,c,之后经过预处理,编译,汇编,链接得到可执行文件。之后在shell中输入启动命令,shell为其fork生成了一个子进程,这就使得hello从程序变成了进程。
Hello的020:由shell加载hello。映射虚拟内存,进入程序,载入物理内存,执行main函数。程序结束后,shell回收hello进程,删除hello的进程占用。
1.2 环境与工具
硬件环境:
处理器 11th Gen Intel(R) Core(TM) i5-11400H @ 2.70GHz 2.69 GHz
机带 RAM 16.0 GB (15.8 GB 可用)
系统类型 64 位操作系统, 基于 x64 的处理器
GPU:NVIDIA GeForce RTX 3050 Laptop GPU
软件环境:
Windows 11
Vmware17上的Ubuntu(64位)虚拟机
开发工具:
vim文本编辑器
Visual Studio
EDB
Vscode
Gcc编译器
调试工具:
GDB
1.3 中间结果
1.hello.i:cpp预处理原始的.c后缀的C程序。主要是将头文件直接插入程序文本中,并且根据define进行字符串的简单替换,得到新的预处理过的C文本文件。(文本文件)
2.hello.s:编译器将预处理过的以.i为后缀的文件编译成汇编文件。(文本文件)
3.hello.o:汇编器将以.s为后缀的汇编文件转化为机器语言,此时为可重定位目标文件(二进制文件)。
4.hello:利用链接器,将C函数库中相应的函数与hello.o可重定位文件链接,生成可执行目标文件(二进制文件)。
5.ybh1.txt:hello.o的用readelf产生的文件。
6.ybh2.txt:hello.o的反汇编文件
7.ybh3.txt:hello用readelf产生的文件。
1.4 本章小结
本章简要介绍了hello的p2p以及020的过程,并简要的介绍了hello.c文本源文件到hello可执行目标文件的转变。并列举了实验进行过程中的硬件环境,软件环境,开发工具,调试工具等。
第2章 预处理
2.1 预处理的概念与作用
预处理概念:处理c源文件中的预处理程序和注释。
预处理作用:预处理器(cpp)根据以字符#开头的预处理命令处理c源文件,修改原始的c源文件,预处理过程中,预处理器会分析预处理指令,依据#include进行库文件的插入,依据#define进行字符串的替换等,并且同时去除c源代码中的注释。
2.2在Ubuntu下预处理的命令
2.3 Hello的预处理结果解析
分析:文件长度变长,预编译指令消失,对应库文件的内容被插入到预处理文件中。
2.4 本章小结
本章执行了预处理指令,查看了预处理后的文本文件的内容,了解了预处理的方式与作用。
第3章 编译
3.1 编译的概念与作用
编译的概念:编译器(ccl)将文本文件.i(C语言)翻译成文本文件.s(汇编语言)。
编译的作用:将C语言文本文件翻译成汇编语言文本文件,每一条汇编文本语言对应一条低级机器指令语言。
3.2 在Ubuntu下编译的命令
3.3 Hello的编译结果解析
3.3.1数据:
printf里的格式字符串:
Main函数的两个参数以及局部变量i:
其中-4(%rbp)为int i的局部变量,-20(%rbp)为mian函数的参数1,-32区域为参数二。
3.3.2赋值:
给局部变量i赋初值0。
3.3.3算数运算:
在循环中给i加1.
3.3.4关系操作:
比较主函数参数1(后边参数个数)和4的关系
循环体中的循环条件。
3.3.5数组/指针/结构操作:
Printf中所需的字符串数组,首地址放入rax中。
前7行是对main函数的字符串数组参数调用,分别调用了第1,2号字符串,最后两行是对printf中格式串字符的引用,都是通过获得首地址来调用数组。
3.3.6控制转移:
对应一下语句
如果等于,进入之后的函数中,如果不等于,则进入exit。
此处为for循环中的条件控制,如果等于8退出,若不等于循环。
3.3.7函数操作:
如果参数个数不等于4,调用printf和exit函数。
在循环体中的printf函数。
调用字符串转数的函数。
调用sleep函数
调用getchar函数。
3.4 本章小结
在本章中通过编译得到了汇编文件,并查看了汇编文件中的汇编语句,对C语言如何用汇编语言实现,汇编与C语言之间的一一对应关系进行了直观的感受。
第4章 汇编
4.1 汇编的概念与作用
汇编的概念:汇编器将.s文本文件翻译成机器语言。
汇编的作用:汇编器将.s中的汇编语言翻译成计算机能处理的机器指令语言,并输出可重定位文件(二进制文件)。
4.2 在Ubuntu下汇编的命令
4.3 可重定位目标elf格式
Elf格式:
重定位节:
类型表示如何引用,R_X86_64_PC32表示重定位PC相对引用,查询互联网后,得知了R_X86_64_PLT32代表了符号在plt项的地址+addend,减去重定位项的offset填入到指令中,PLT貌似是一个函数入口表的格式。R_X86_64_32是重定位绝对引用的意思。
4.4 Hello.o的结果解析
汇编中使用的是十进制的立即数,反汇编得到的为十六进制的立即数。汇编中使用节+偏移表示地址,反汇编使用main+偏移表示转移地址,且没有L1等节。机器识别由操作码和操作数组成的机器语言,机器语言和汇编语言是一对一的映射关系。
4.5 本章小结
本节利用汇编器得到了可重定位的目标文件,并对汇编产生的可重定位目标文件进行了分析,看了可重定位文件的各个节,各节的内容和作用;比较了机器语言和汇编代码的关系,分析了反汇编和汇编的关系。
第5章 链接
5.1 链接的概念与作用
链接的概念:合并各个独立的但互相引用的文件。
链接的作用:将多个目标文件进行连接构成可执行目标文件,为的是可以在一个文件中使用另一个文件中定义的函数/全局变量。
5.2 在Ubuntu下链接的命令
5.3 可执行目标文件hello的格式
各段地址大小如下图:(节头部表)
5.4 hello的虚拟地址空间
加载hello后的虚拟地址空间:
对照5.3中的.init节,从0x00401000开始
对照rodata节,从0x00402000开始:从依稀出现的hello看出此处存了一些printf中的格式串。
5.5 链接的重定位过程分析
链接后的可执行文件中相比于连接前的可重定位文件,多了许多函数,且call语句后面为正确的执行位置。而非下一条语句的位置。
链接过程为:链接器根据hello.o文件中重定位条目中重定位元素的类型的不同得到不同的偏移量,通过节偏移和重定位条目的地址就可以计算出重定位后的地址;最基本的重定位类型有两种,分别是重定位一个使用32位PC相对地址的引用和重定位一个使用32位绝对地址的引用。得到地址后根据hello.o中的提示对地址进行修改。就可以完成重定位,之后链接hello运行所需的其他库函数。
5.6 hello的执行流程
hello程序先调用_init初始化,之后到_start,再到main,main执行之后执行_printf、_exit、_atoi、_sleep、_getchar,最后退出。
程序名和地址:
0000000000401000 <_init>
0000000000401125 <main>
00000000004010f0 <_start>
0000000000401090 <puts@plt>
00000000004010a0 <printf@plt>
00000000004010b0 <getchar@plt>
00000000004010c0 <atoi@plt>
00000000004010d0 <exit@plt>
00000000004010e0 <sleep@plt>
5.7 Hello的动态链接分析
动态链接采用了延迟加载的策略,即在调用函数时才进行符号的映射。使用偏移量表GOT+过程链接表PLT实现函数的动态链接。GOT中存放函数目标地址,为每个全局函数创建一个副本函数,并将对函数的调用转换成对副本函数调用。
在之前的节头表中发现got.plt地址为404000。
dl_init前:
dl_init后:
可见其中个别字节发生了改变,是由于动态链接产生的结果。
5.8 本章小结
本章通过链接得到了可执行目标文件并分析了hello程序运行时的虚拟地址空间,hello的重定位过程和执行过程中地址的变化。阐述了动态链接的过程。
第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:对处理器,主存,I/O设备的抽象,一个执行中程序的实例。
进程的作用:给我们一种假象,仿佛在计算机上,只有我们的程序是当前唯一运行的程序,我们的程序独占处理器,独占内存。
6.2 简述壳Shell-bash的作用与处理流程
作用:shell是一个为用户提供界面访问操作系统的内核服务。它是用户使用linux的方式。
处理流程:
1.从终端输入程序
2.切分字符串获得参数
3.如果内置命令则立刻执行,否则调用相应的程序为其分配子进程并运行
6.3 Hello的fork进程创建过程
在终端输入命令行后,shell会处理命令,如果不是内置命令,则调用fork创建子进程,子进程与父进程有相同虚拟地址,文件文件描述的相同副本。但PID不同。
6.4 Hello的execve过程
Execve在当前进程中加载并运行包含在可执行目标文件hello的程序,代替当前程序。首先删除已存在的用户区域,之后映射共享部分,最后设置程序计数器。
6.5 Hello的进程执行
1.逻辑控制流:程序计数器(PC)值的序列叫做逻辑控制流,简称为逻辑流。
2.上下文切换:上下文是内核重新启动一个被抢占进程所需的状态。内核可以决定抢占当前的进程,并重新开始一个先前被抢占的进程,这种决策成为调度,由调度器执行。在内核中调度一个新的程序运行,它会抢占当前的进程,该过程中使用的机制成为上下文切换,是一种较高层次的一场控制流来实现的。
3.时间片:进程执行它的控制流的一部分的每一个时间段称为时间片。
Shell调用fork为hello创建子进程,使用execve进行运行,开始hello运行在用户模式,收到信号后进入内核模式,运行信号的处理程序,之后回到用户模式。此时上下文切换,切分成时间片,并且也会切入到其他进程,形成多个程序共用处理器,但给其以独占cpu的感觉。
6.6 hello的异常与信号处理
可能出现的异常:
- 中断:处理器外I/O设备信号的结果,异步异常,总是返回到下一条指令
- 陷阱:一种有意的异常,使指令执行的结果,其最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,称为系统调用。一般返回下一条指令。
- 由错误的情况引起,非有意。如果被故障处理程序修正,则重新执行;若不能被修正,则终止程序。
- 不可恢复的致命性错误。终止处理程序,并且不会控制返回给应用程序。
信号:
Hello中的信号处理:
1.Ctrl+C结果:终止
2.乱摁和回车:正常运行。
3.Ctrl+Z结果:停止
4.ps:显示进程
5.jobs:显示任务
6.pstree:显示进程树
7. fg:后台停止程序变为前台运行程序
8.kill:发送信号
发送杀死信号:停止程序变为终止程序
发送SIGCONT:停止程序继续运行
6.7本章小结
本章概括了进程的概念,shell的作用,fork和execve函数的运行过程以及hello的异常和信号处理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:由程序产生的和段相关的偏移地址,格式为:段+偏移。
线性地址:逻辑地址加上相应段的基地址就生成了线性地址。
虚拟地址:虚拟地址是一个抽象的地址空间,虚拟地址对应虚拟页,虚拟页会映射磁盘空间的一页,如果要使用该页上的数据,则会将该页载入内存,虚拟地址就对应了物理地址。
物理地址:CPU外部地址总线上的物理内存的地址,可以将内存看成一个从0字节开始的数组,每个字节拥有单独的物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段式管理:逻辑地址->线性地址==虚拟地址
被选中的段描述符先被送至描述符cache,每次从描述符cache中取32位段基址,与32位段内偏移量(有效地址)相加得到线性地址
7.3 Hello的线性地址到物理地址的变换-页式管理
虚拟内存分割为虚拟页,物理内存分割为物理页,依据虚拟地址的虚拟页号和页偏移得到物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
通过虚拟地址得到一级页表中下一级页表的位置,以此类推至4级页表,最后得到的是物理地址。
7.5 三级Cache支持下的物理内存访问
首先通过物理地址中的组索引位得到组号。先在L1高速缓存对比有效位和标志位,匹配成功则读取,失败则进入二级高速缓存中再次匹配,以此类推,并且进行缓存的更改,在k-1层存储中缓存该数据。
7.6 hello进程fork时的内存映射
为新进程创建虚拟内存,创建当前进程的的mm_struct, vm_area_struct和页表的原样副本.,两个进程中的每个页面都标记为只读,两个进程中的每个区域结构都标记为私有的写时复制。在新进程中返回时,新进程拥有与调用fork进程相同的虚拟内存,随后的写操作通过写时复制机制创建新页面。
7.7 hello进程execve时的内存映射
为hello程序删除已存在的用户区域,创建新的区域结构,代码和初始化数据映射到.text和.data区(目标文件提供),bss和栈堆映射到匿名文件 ,栈堆的初始长度0,共享对象由动态链接映射到本进程共享区域,设置PC,指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
缺页处理程序的主要步骤为:
1.检查虚拟地址是否合法,若不合法,则程序终止。
2.检查进程是否有读写或执行该区域权限,若不具有,则触发异常保护机制,程序终止。
3.内核选择一个牺牲页面,写入磁盘,重新更换新的页面。
4.控制权转移给hello进程,执行触发缺页的指令。
7.9本章小结
本章阐述了计算机中虚拟内存管理,物理地址,线性地址,逻辑地址以及他们的变换模式,段式,页式管理,在内存映射的基础上重新认识了fork和execve函数。
结论
Hello的程序人生:
1.hello.c C语言文本文件的编写
2.预处理过程:hello.c预处理为hello.i
3.编译:编译器(ccl)将文本文件hello.i(C语言)翻译成文本文件hello.s(汇编语言)。
4.汇编:汇编器将hello.s中的汇编语言翻译成计算机能处理的机器指令语言,并输出可重定位文件hello.o(二进制文件)
5.链接:将hello中使用的库函数文件与hello.o文件进行连接构成可执行目标文件。
6.Shell为hello创建进程
7.Shell运行hello
8.运行指令,读取内存,处理信号和异常。
9.结束运行,shell回收hello进程。
通过对hello程序在计算机上执行的过程进行梳理,我对于计算机底层实现有了系统的了解。
附件
1.hello.i:cpp预处理原始的.c后缀的C程序。主要是将头文件直接插入程序文本中,并且根据define进行字符串的简单替换,得到新的预处理过的C文本文件。(文本文件)
2.hello.s:编译器将预处理过的以.i为后缀的文件编译成汇编文件。(文本文件)
3.hello.o:汇编器将以.s为后缀的汇编文件转化为机器语言,此时为可重定位目标文件(二进制文件)。
4.hello:利用链接器,将C函数库中相应的函数与hello.o可重定位文件链接,生成可执行目标文件(二进制文件)。
5.ybh1.txt:hello.o的用readelf产生的文件。
6.ybh2.txt:hello.o的反汇编文件
7.ybh3.txt:hello用readelf产生的文件。
参考文献
[1] 袁春风. 计算机系统基础. 北京:机械工业出版社,2018.7(2019.8重印)
[2] Randal E. Bryant;David R. O’Hallaron. 深入理解计算机系统. 北京:机械工业出版社,2016.7(2019.3重印)
[5] 一个简单程序从编译、链接、装载(执行)的过程-静态链接 - Lingcs的文章 - 知乎一个简单程序从编译、链接、装载(执行)的过程-静态链接 - 知乎
[6] https://blog.csdn.net/drshenlei/article/details/4261909