以一个个简单的程序hello.c为样本,通过对它的从创建到结束的整个历程进行分析,分析研究hello程序在Linux下的P2P和020过程,进一步了解预处理、编译、汇编、链接和可执行文件执行过程中的进程管理、存储空间管理和I/O管理的原理,更深一步地了解程序运行的过程。
关键词:预处理;编译;汇编;链
目 录
第1章 概述
1.1 Hello简介
P2P: From Program to Process,要生成可执行文件,首先要进行预处理,Hello.c经c预处理器转化为hello.i;然后经c编译器转化为hello.s,再经汇编器转化为hello.o生成汇编代码;最后链接器将该文件与头文件中所需的库函数链接形成可执行文件。在shell下输入./hello运行,shell为其运行fork,产生子进程,由此将program变成一个progress。
O2O: From Zero-0 to Zero-0,shell执行可执行文件,管理hello进程,存储,操作系统OS为其映射虚拟内存空间、分配物理内存。代码执行后,父进程回收hello进程,并且内核会从系统中删除hello所有痕迹。
1.2 环境与工具
1.2.1 硬件环境
设备名称 LAPTOP-5I0F3CCS
处理器 11th Gen Intel(R) Core(TM) i7-11370H @ 3.30GHz 3.30 GHz
机带 RAM 16.0 GB (15.7 GB 可用)
设备 ID 2D004F51-7552-4975-8B67-367F4E860F30
产品 ID 00342-36116-50107-AAOEM
系统类型 64 位操作系统, 基于 x64 的处理器
笔和触控 没有可用于此显示器的笔或触控输入
1.2.2 软件环境
版本 Windows 10 家庭中文版
版本号 21H1
安装日期 2021/2/12
操作系统内部版本 19043.1586
体验 Windows Feature Experience Pack 120.2212.4170.0
1.2.3 开发工具
Vscode VS gcc ,gdb
1.3 中间结果
hello.c:源代码
hello.i:预处理后文本文件
hello.s:编译后汇编文件
hello.o:汇编后可重定位目标执行文件
hello.elf:hello.o的ELF格式
hello:链接后可执行文件
1.4 本章小结
介绍了关于hello.c的P2P和O2O机制,列出了软件环境、硬件环境、开发与调试工具与工具还有中间的结果
第2章 预处理
2.1 预处理的概念与作用
概念:预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。
作用:根据源代码中的预处理指令修改源代码,预处理从系统的头文件包中将头文件的源码插入到目标文件中,宏和常量标识符已全部被相应的代码和值替换,最终生成.i文件
2.2在Ubuntu下预处理的命令
cpp hello.c >hello.i
2.3 Hello的预处理结果解析
经过预处理之后,原本23行的代码被扩展为3105行,并且原本的注释也被删除,原本的代码被放置在3092-3105行,而且main 函数没有变化。#include <stdio.h> #include <stdlib.h> #include <unistd.h>三个头文件消失前面大部分的代码应该是处理“#include”预编译指令,将被包含的文件插入到该预编译指令的位置
Hello.c
Hello.i 的main函数部分
Hello.i递归展开的头文件
2.4 本章小结
主要介绍了预处理的概念及作用,以及在Ubuntu下进行预处理,对得到的hello.i进行解析。
第3章 编译
3.1 编译的概念与作用
概念:编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。
作用:把用高级程序设计语言书写的源程序,翻译成等价的计算机汇编或机器语言书写的目标程序,输出汇编或机器语言表示的目标程序。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
Hello.s
3.3 Hello的编译结果解析
3.3.1 数据
3.3.1.1 字符串
.LC0 和.LC1处可以发现两个字符串
.rodata表示只读,中文被编码为UTF-8格式 . LC0对应.LC1对应
3.3.1.2参数argc与argv
argc被放到了-20(%rbp)的位置,而argv是被放到了-32(%rbp)的位置
3.3.1.3 局部变量i
对应.L2,初始化为0,并分配空间
循环+1,并与8比较
下图是jump到.L4并+1
下图是循环条件、
3.3.1.4 立即数
在汇编代码中以$<立即数>的形式表示
下图为示例
3.3.2 操作
3.3.2.1 赋值操作
对应.L2,初始化为0,
利用mov指令,把数据存入内存或寄存器
3.3.2.2 类型转换
在hello.c中利用atoi进行类型转换
下图为汇编语言对应
利用call调用
3.3.2.3 算数操作
利用add指令,对i加1
3.3.2.4 关系操作与控制转移
上图为 局部变量 i的循环判定
下图是参数argc与4的比较
if语句直接使用条件跳转实现
3.3.2.5 数组/指针/结构操作
argv作为一个指针数组,每个地址是8,将位argv[1]和argv[2]传给printf(在其中%rdx和%rsi是第一、二个参数,%rax先变成argv[1]、[2]的地址,然后从内存中取值并复制给参数寄存器)
3.3.2.6函数操作
call语句用来进行函数调用,ret将返回值返回给函数
1.exit
2.printf
3.sleep
4.getchar
5.Atoi
此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析。
3.4 本章小结
理解编译的概念和作用,对hello.i进行编译,然后分析了编译得到的文件hello.s,理解机器指令,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。
第4章 汇编
4.1 汇编的概念与作用
概念:汇编器(as)将hello.s翻译成机器语言指令,将这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在hello.o中。
作用:把汇编语言翻译成机器语言。
4.2 在Ubuntu下汇编的命令
as hello.s -o hello.o
4.3 可重定位目标elf格式
readelf -a hello.o > hello.elf。
- ELF Header:用于概述ELF文件各信息的段,可与根据此代码段知道该文件是可重定位文件还是可执行文件。
- Section Header:描述.o文件出现的各节的类型、位置和空间大小等信息。
- .rela.text节:重定位节在链接时,根据这个表格对各个段引用的外部符号修改。链接器会根据重定位条目类型和偏移量计算正确的地址值,并进行修改。
- Symbol table:符号表存放在程序定义和引用的函数和全局变量的信息。
4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
- 函数调用机制不同。hello.s在调用函数时是直接用 call+函数名的方式,直接跳转到函数名。
而hello.o文件得到的反汇编代码中是在call之后用的对应命令在main函数中的偏移量。
跳转标识不同。Hello.s中的跳转采用跳转到.L2等方式进行跳转,而反汇编文件中则用具体的地址表示,跳转位置为主函数起始地址加上偏移量。
- 立即数和地址中数字表示不同。Hello.s中的操作数采用10进制编码
,而反汇编文件中的操作数都已16进制编码
4.5 本章小结
概述了汇编的概念和作用,分析了ELF格式文件的内容,另外比较了重定位前汇编程序和重定位后反汇编的汇编程序差别,了解到了一个汇编语言文件是如何通过汇编器翻译成机器可识别的机器语言的。
第5章 链接
5.1 链接的概念与作用
概念:链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载到内存并执行。并且完成相应的符号解析和重定位的,将其组织成一个可执行文件的过程。
作用:链接是由叫做链接器的程序执行的,链接器使得分离编译成为可能。
5.2 在Ubuntu下链接的命令
ld -o hello -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o hello.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
ELF Header
.rela.text节:重定位节
section header
Symbol table:符号表
5.4 hello的虚拟地址空间
edb图中第一行为ELF头信息,也就是ELF文件最开始存的数据。所以edb的堆的第一行和elf相等。
5.5 链接的重定位过程分析
hello与hello.o的不同
- 在hello中,新增了.init、.plt和.plt.sec三个节
2.新增了在hello.c中用到的库函数
3. hello的地址全是虚拟地址,hello.o的是相对地址
Hello
Hello.c
链接的重定位过程分析
与hello.o生成的反汇编文件相比,helloout.txt中多了许多节。编译器和汇编器通过合并各链接文件的节,生成从地址0开始的代码和数据节。在使用ld命令链接的时候,指定了动态链接器为64的/lib64/ld-linux-x86-64.so.2。在动态共享库libc.so中,定义了hello需要的printf、sleep、getchar、exit 函数和_start 中调用的 __libc_csu_init,__libc_csu_fini,__libc_start_main。这些函数被链接器加入其中。hello.o反汇编代码就直接是.text段,然后为main函数。而hello反汇编的结果中,由于链接过程中重定位而加入进来各种函数、数据。如开始的函数和调用的函数填充在main函数之前。所以main函数的位置发生了巨大的改变。
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
5.7 Hello的动态链接分析
调用共享库函数时,由于定义它的共享模块在运行时可以加载到任意位置,编译器没有办法预测这个函数的运行时地址。应为该引用生成一条重定位记录,然后动态链接器在程序加载时再解析它,找到它的地址。 在调用dl_init之后,.got.plt段的数据会变化,查看.got.plt段信息,
在dl_init调用之前,对于每一条PIC函数调用,调用的目标地址都实际指向PLT中的代码逻辑,初始时每个GOT条目都指向对应的PLT条目的第二条指令。动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
在调用dl_init之后,.got.plt段的数据会变化
5.8 本章小结
了解了在Linu系统中链接的运行机制。通过将目标文件hello.o链接为hello可执行文件,我们更加了解了关于链接器的工作细节,并且对比了可执行文件与.o文件的elf文件结构,加深了对于链接、重定位等过程的认识。
第6章 hello进程管理
6.1 进程的概念与作用
概念:
进程是一个执行中的程序的实例。
作用:
通过进程,我们会得到一种假象,好像我们的程序是当前唯一运行的程序,我们的程序独占处理器和内存,我们程序的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
Shell是一个命令解释器,它解释由用户输入的命令并且把它们送到内核。不仅如此,Shell有自己的编程语言用于对命令的编辑,它允许用户编写由shell命令组成的程序。读取用户的输入,获得输入参数如果是内核命令则直接执行,否则调用相应的程序执行命令在程序运行期间,shell需要监视键盘的输入内容,并且做出相应的反应,Bash是一个命令处理器,通常运行于文本窗口中,并能执行用户直接输入的命令
处理流程:
第一步:用户输入命令。
第二步:shell对用户输入命令进行解析,判断是否为内置命令。
第三步:若为内置命令,调用内置命令处理函数,否则调用相应的程序执行命令
第四步:判断是否为前台运行程序,如果是,则调用等待函数等待前台作业结束;否则将程序转入后台,直接开始下一次用户输入命令。
第五步:shell应该监视键盘的输入内容,并对这些信号进行相应处理
6.3 Hello的fork进程创建过程
首先shell对命令行进行切分,存入字符串数组,再检查hello是否为内置命令,发现不是,于是调用fork函数创建子进程,子进程大部分与父进程相同,但其pid与父进程不同。通过fork函数,子进程得到与父进程用户级虚拟地址空间相同的独立副本。
6.4 Hello的execve过程
子进程会调用execve函数,其参数主要是两个二级指针char **argv , char **envp,其作用主要是给出加载程序的参数。只有在加载文件出现错误的时候,例如找不到目标文件,execve函数才会返回,否则就直接执行程序,不再返回。在程序头部表的引导下,加载器将可执行文件的片复制到代码段和数据段。接下来,加载器跳转到程序的入口点,也就是_start函数的地址。这个函数是在系统目标文件ctrl.o中定义的,对所有C程序都是一样的。start函数调用系统启动函数 _libc_start_main,该函数定义在libc.so中,它初始化执行环境,调用用户层的main函数,处理main函数的返回值,并且在需要的时候把控制返回给内核。
6.5 Hello的进程执行
系统中的每个程序都运行在某个进程的上下文中。上下文由程序正确运行所需的状态组成,这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。内核调度hello的进程开始进行,输出Hello与之前输入的内容,然后执行sleep函数,这个函数是系统调用,它显示地请求让调用进程休眠。内核转而执行其他进程,这时就会发生一个上下文转换。当调用sleep之前,如果hello程序不被抢占则顺序执行,假如发生被抢占的情况,则进行上下文切换,上下文切换是由内核中调度器完成的,当内核调度新的进程运行后,它就会抢占当前进程,并保存以前进程的上下文,恢复新恢复进程被保存的上下文将控制传递给这个新恢复的进程,来完成上下文切换。在hello进程调用sleep之后陷入内核模式,内核处理休眠请求主动释放当前进程,并将hello进程从运行队列中移出加入等待队列,定时器开始计时,内核进行上下文切换将当前进程的控制权交给其他进程,2s后,又会发生一次进程转换,恢复hello进程的上下文,继续执行hello进程。重复9次这个过程。
循环结束后,后面执行到getchar函数,当数据已经被读取到缓存区中,将会发生一个中断,使内核发生上下文切换,重新执行hello进程。
6.6 hello的异常与信号处理
- 异常的信号以及异常种类
1.1中断
来源于I/O设备的信号,其总是返回下一条指令
1.2陷阱
有意的异常,总是返回下一条指令。
1.3故障
进程发生了一些错误, 返回当前导致故障的指令使其重新执行或者处理故障失败,程序报错并且终止。
1.4中止
发生了一个不可以被修复的错误,这种异常是同步的,且不会返回
2.
- 回车
回车对于程序运行并没有什么影响
2.Ctrl-Z
3.Ctrl-C
结束 hello。我们输入ps指令,发现查询不到hello进程的PID,输入指令jobs,发现也没有对应作业,因此hello进程被彻底终止。
4.乱按
对于程序运行并没有什么影响
5.Ctrl-Z后可以运行ps jobs pstree fg kill
1. ps jobs fg 用ps指令查看其进程PID,可以发现hello的PID是30527;再用jobs查看此时hello的后台 job号是1,调用指令fg 1将其调回前台。
2.Kill kill指令向所在的挂起的进程发出终止指令
3.Pstree
6.7本章小结
介绍了进程的概念和作用,描述了shell如何在用户和系统内核之间建起一个交互的桥梁进一步地巩固了关于hello作为进程在运行的时候的一些相关的知识。
第7章 hello的存储管理
7.1 hello的存储器地址空间
(以下格式自行编排,编辑时删除)
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
7.2 Intel逻辑地址到线性地址的变换-段式管理
(以下格式自行编排,编辑时删除)
7.3 Hello的线性地址到物理地址的变换-页式管理
(以下格式自行编排,编辑时删除)
7.4 TLB与四级页表支持下的VA到PA的变换
(以下格式自行编排,编辑时删除)
7.5 三级Cache支持下的物理内存访问
(以下格式自行编排,编辑时删除)
7.6 hello进程fork时的内存映射
(以下格式自行编排,编辑时删除)
7.7 hello进程execve时的内存映射
(以下格式自行编排,编辑时删除)
7.8 缺页故障与缺页中断处理
(以下格式自行编排,编辑时删除)
7.9动态存储分配管理
(以下格式自行编排,编辑时删除)
Printf会调用malloc,请简述动态内存管理的基本方法与策略。
7.10本章小结
(以下格式自行编排,编辑时删除)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
(以下格式自行编排,编辑时删除)
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
(以下格式自行编排,编辑时删除)
8.3 printf的实现分析
(以下格式自行编排,编辑时删除)
[转]printf 函数实现的深入剖析 - Pianistx - 博客园
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
(以下格式自行编排,编辑时删除)
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
(以下格式自行编排,编辑时删除)
结论
1. 预处理: hello.c预编译成hello.i
2. 编译 hello.i编译得到hello.s汇编文件
3. 汇编 hello.s汇编得到二进制可重定位目标文件hello.o
- 链接,将hello.o与可重定位目标文件和动态链接库链接成为可执行目标程序hello
5. 运行:shell中运行hello shell调用fork生成子进程,由execve执行,在上下文文件中加载hello
6. 访问内存:MMU将程序中使用的虚拟内存地址通过页表映射成物理地址。
7. 动态申请内存:printf会调用malloc向动态内存分配器申请堆中的内存。
8. 信号:如果运行途中键入中断,则调用shell的信号处理函数分别停止、挂起。
- 结束:shell父进程回收子进程,内核删除为这个进程创建的所有数据结构。如果父程序不能回收该子程序,则由中断直接收回
附件
hello.c:源代码
hello.i:预处理后文本文件
hello.s:编译后汇编文件
hello.o:汇编后可重定位目标执行文件
hello.elf:hello.o的ELF格式
hello:链接后可执行文件
列出所有的中间产物的文件名,并予以说明起作用。
参考文献
为完成本次大作业你翻阅的书籍与网站等
- 进程的概念和特征_C语言中文网
- CSAPP
- SSE指令集简介_进击的路飞桑的博客-优快云博客_sse指令集
- cpu性能优化_以下是一些简单的优化措施,可将性能降低到CPU之外_dfsgwe1231的博客-优快云博客
]