计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 人工智能(未来技术)
学 号 7203610730
班 级 2036014
学 生 郭果通
指 导 教 师 刘宏伟
计算机科学与技术学院
2021年5月
摘 要
本文通过分析一个简单的C语言程序在linux系统中从预处理到结束的全过程,加深我们对计算机系统的认识与理解。本文从hello的自白开始,对hello预处理、编译、汇编、链接、hello的进程管理、存储管理、IO管理等相关内容进行了深入的分析,并在linux系统上对该过程的每一步进行实验。最终使我们真正对hello的产生到回收的整个过程有了深刻的理解,从而让我们感受到学习计算机系统的意义和精髓所在。
关键词:linux;hello;预处理;编译;汇编;进程;存储
目 录
第1章 概述... - 4 -
1.1 Hello简介... - 4 -
1.2 环境与工具... - 4 -
1.3 中间结果... - 5 -
1.4 本章小结... - 5 -
第2章 预处理... - 6 -
2.1 预处理的概念与作用... - 6 -
2.2在Ubuntu下预处理的命令... - 6 -
2.3 Hello的预处理结果解析... - 6 -
2.4 本章小结... - 7 -
第3章 编译... - 9 -
3.1 编译的概念与作用... - 9 -
3.2 在Ubuntu下编译的命令... - 9 -
3.3 Hello的编译结果解析... - 9 -
3.4 本章小结... - 13 -
第4章 汇编... - 14 -
4.1 汇编的概念与作用... - 14 -
4.2 在Ubuntu下汇编的命令... - 14 -
4.3 可重定位目标elf格式... - 14 -
4.4 Hello.o的结果解析... - 17 -
4.5 本章小结... - 19 -
第5章 链接... - 20 -
5.1 链接的概念与作用... - 20 -
5.2 在Ubuntu下链接的命令... - 20 -
5.3 可执行目标文件hello的格式... - 20 -
5.4 hello的虚拟地址空间... - 22 -
5.5 链接的重定位过程分析... - 24 -
5.6 hello的执行流程... - 26 -
5.7 Hello的动态链接分析... - 28 -
5.8 本章小结... - 29 -
第6章 hello进程管理... - 30 -
6.1 进程的概念与作用... - 30 -
6.2 简述壳Shell-bash的作用与处理流程... - 30 -
6.3 Hello的fork进程创建过程... - 30 -
6.4 Hello的execve过程... - 31 -
6.5 Hello的进程执行... - 32 -
6.6 hello的异常与信号处理... - 33 -
6.7本章小结... - 37 -
第7章 hello的存储管理... - 38 -
7.1 hello的存储器地址空间... - 38 -
7.2 Intel逻辑地址到线性地址的变换-段式管理... - 38 -
7.3 Hello的线性地址到物理地址的变换-页式管理... - 39 -
7.4 TLB与四级页表支持下的VA到PA的变换... - 40 -
7.5 三级Cache支持下的物理内存访问... - 40 -
7.6 hello进程fork时的内存映射... - 40 -
7.7 hello进程execve时的内存映射... - 40 -
7.8 缺页故障与缺页中断处理... - 40 -
7.9动态存储分配管理... - 40 -
7.10本章小结... - 40 -
第8章 hello的IO管理... - 41 -
8.1 Linux的IO设备管理方法... - 41 -
8.2 简述Unix IO接口及其函数... - 41 -
8.3 printf的实现分析... - 41 -
8.4 getchar的实现分析... - 41 -
8.5本章小结... - 41 -
结论... - 42 -
附件... - 43 -
参考文献... - 44 -
hello的整个过程指的是hello从程序员编写到在计算机系统里面运行所经历的一系列步骤。
首先P2P指的是:From Program to Process。即hello.c从程序变为进程的这个过程。
该过程一共分为以下6步:
接下来020指的是:From Zero-0 to Zero-0,指的是hello从最开始没有空间到最后也被从进程中删除这一过程。在用户在Shell中敲下./hello之前按,hello不占用内存空间,即对应第一个0,接下来shell使用fork以及execve加载hello,然后mmap为其分配虚拟内存空间,接下来由Cache,CPU等硬件资源配合运行程序,直到进程结束。第二个0指的是在进程结束后,hello进程会被父进程回收,并由内核删除相关的数据结构,最终hello不再占用内存空间。
硬件:处理器:AMD Ryzen 7 4800H with Radeon Graphics 2.90 GHz
RAM : 16GB 系统类型:64 位操作系统, 基于 x64 的处理器
软件 Windows10 64位
Ubuntu 20.04.4
开发与调试工具:vscode,Visual Stdio,2022 ,gcc,edb,gedit
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
表格 1 中间产物
文件名 | 作用 |
hello.i | hello.c预处理后得到的文件 |
hello.s | 编译后的汇编语言文件 |
hello.o | 汇编后得到的可重定位目标文件 |
hello.elf | 用readelf读取hello.o得到的ELF格式信息 |
Dhello.s | 反汇编hello.o得到的反汇编文件 |
helloout.elf | 由hello可执行文件生成的.elf文件 |
Dhelloout.s | 反汇编hello可执行文件得到的反汇编文件 |
hello | 最终链接得到的可执行文件 |
(第1章0.5分)
本章首先讲述了Hello的P2P,020的整个过程的具体含义,然后介绍了完成该论文时所采用的环境与工具以及生成的中间结果。
预处理的概念:
预处理是指编译器在读取源程序后,在真正的编译开始之前,预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。主要包括宏定义、文件包含、条件编译三方面的内容。例如hello.c程序的第6、7、8行#include
预处理的作用:
在Ubuntu系统下,进行预处理的命令为:gcc -E hello.c -o hello.i
图 1 预处理
打开预处理产生的hello.i文件
图2 hello.i文件
将之与源程序hello.c进行对比:
图3 hello.c源程序
通过对比可以看到,预处理将源程序进行了扩展补充,hello.i中的3047-3060行为main函数部分,可以看到在预处理的过程中,预处理器引入了头文件包含的内容,将所有宏替换为对应的值,并删除了注释内容。
本章介绍了预处理相关的内容,并在Ubuntu上进行验证,通过对hello.c和hello.i进行比较,从而清楚地认识到预处理器对源程序的作用,对预处理有了更深入的认识。
编译的概念:编译器将目标源程序hello.i翻译成汇编语言程序hello.s。
编译的作用:为不同的高级语言编写的源程序提供了通用的低级汇编语言输出,便于所有的机器以相同的标准执行。
图 4 编译得到hello.s文件
并产生了hello.s文件
3.3.1数据
3.3.1.1常量
图5 hello.s文件内容
可以看到,在hello.s中,常量以立即数的形式存在。
例如if(argc!=4)在hello.s中表示为:
cmpl $4 ,-20(%rbp)
正在上传…重新上传取消
4为常量,在汇编语言中,$4表示立即数,所以常量4在hello.s中以立即数形式存在,此外,对for循环中出现的常量0,8等,均用立即数表示:
图6 使用立即数表示的例子
此外,该程序中还有两个字符串:
图 7 程序中的字符串
3.3.1.2局部变量
容易看出,只有一个局部变量i,通过int i进行定义,其在运行过程中保存
栈中。并在for循环中for(i&#61;0;i<8;i&#43;&#43;)中完成赋值&#xff0c;比较和加一操作&#xff0c;对应汇编语言如下所示&#xff1a;i储存于-4(%rbp)对应的栈中。
movl $0, -4(%rbp) //将i赋初值为0
cmpl $7, -4(%rbp) //将i和7比较
addl $1, -4(%rbp) //将i加1
3.3.2操作
3.3.2.1赋值
赋值涉及到对局部变量i进行赋值&#xff0c;对应的汇编语句为:
movl $0, -4(%rbp) //将i赋初值为0
此外&#xff0c;mov为赋值操作,movl表示对32位4字节的数据进行赋值
3.3.2.2 算数操作
1.加减法
加减法涉及到的汇编指令如下所示
addl $1, -4(%rbp) //i&#43;&#43;
subq $32, %rsp //减法指令&#xff1a;栈帧-8
addq $8, %rax //数组偏移
3.3.2.3 关系操作
关系操作即所谓的比大小&#xff0c;在原源程序中涉及到的关系操作对应的语句有&#xff1a;
if(argc!&#61;4)
for(i&#61;0;i<8;i&#43;&#43;)
对应hello.s中的汇编指令分别为&#xff1a;
cmpl $4, -20(%rbp)
cmpl $7, -4(%rbp)
其中cmpl表示将两个32位的数据进行比较
3.3.2.4控制转移
控制转移一般和关系操作联系紧密&#xff1a;
hello.s中涉及到的转移有&#xff1a;
je .L2
意思是如果argc!4&#xff0c;则跳转到.L2&#xff0c;即执行if内的语句
jle .L4
对应的是for循环语句&#xff0c;每次执行循环体后进行比较操作&#xff0c;如果不满足i<&#61;7&#xff0c;则跳出循环体
3.3.2.5 数组和指针操作
在汇编语言中&#xff0c;用下标访问数组元素和指针操作都涉及到对栈或者内存的访问&#xff0c;两者的实现方式一样。
通过.c程序可以看到&#xff0c;这里涉及到有main函数中的形参中的指针数组char *argv[]&#xff0c;以及argv[1],argv[2],argv[3]。在hello.s中查看得&#xff1a;movq %rsi, -32(%rbp)
图 8 涉及到数组和指针的操作
从而可以看出实现对数组元素得访问利用了movq命令&#xff0c;其中q是指指针的数据大小为8个字节&#xff0c;所以变址也为8的倍数。所以&#xff0c;指针和数组的访问其实非常简单&#xff0c;应注意涉及到的mov命令&#xff0c;以及对应的基址变址法操作等便可以轻松做出判断。
3.3.2.6 函数操作
函数操作所涉及到的内容有参数传递(地址/值)、函数调用()、局部变量、函数返回
参数传递&#xff1a;传入参数argc和argv[]&#xff0c;分别用寄存器%rdi和%rsi存储。
函数调用&#xff1a;作为主函数&#xff0c;由系统调用
局部变量&#xff1a;int i
函数返回&#xff1a;在C程序中&#xff0c;return 0可得返回值是0&#xff0c;在汇编语句中%rax储存的是返回值。通过movl $0, %eax将%eax设置为0&#xff0c;返回0&#xff0c;对应return 0.
2.printf函数
程序第一次调用printf对应的汇编语句为
图9 printf对应的汇编语句
其中通过leaq .LC0(%rip), %rdi将参数字符串地址传给%rdi&#xff0c;由main函数进行在if语句中调用&#xff0c;汇编代码为 call puts&#64;PLT
.LC0为
图10 .LC0语句
第二次调用printf函数为&#xff1a;
图11 第二次调用printf函数部分
这里涉及到三个参数&#xff0c;argv[1]与argc[2]则分别将首地址传给%rsi与%rdx。
.LC1为&#xff1a;
图12 .LC1语句
3.exit函数
对应的汇编程序如下&#xff1a;
图 13 exit函数对应的汇编程序
参数传递&#xff1a;通过movl $1, %edi传递
函数调用&#xff1a;当argc!&#61;4时调用&#xff0c;call exit&#64;PLT为调用的汇编命令。
4. sleep函数
对应的汇编程序如下&#xff1a;
图 14 sleep函数对应的汇编语句
源程序如下&#xff1a;sleep(atoi(argv[3]));
参数传递&#xff1a;参数传递内容如上图前三行所示&#xff0c;将atoi函数的返回值作为参数然后传入sleep函数中
函数调用&#xff1a;在for循环中每次都有调用
5.atoi函数&#xff1a;
源程序为&#xff1a;atoi(argv[3])&#xff0c;对应的汇编语句为&#xff1a;
图 15 atoi函数对应的汇编语句
参数传递&#xff1a;可以看到argv[3]的字符串地址传入%rdi.
函数调用&#xff1a;由main函数调用&#xff0c;每次for循环循环调用&#xff0c;利用call atoi&#64;PLT语句调用。
6. getchar函数&#xff1a;
源程序为&#xff1a;getchar()&#xff0c;对应的汇编语句为&#xff1a;
图 16 getchar函数对应的汇编语句
该函数没有参数&#xff0c;由main函数调用,通过call getchar&#64;PLT调用&#xff0c;函数的返回值为返回读取字符的ASCLL
本章首先介绍了编译的概念和作用是将hello.i程序编译为由汇编语言表示的hello.s程序&#xff0c;然后在Ubuntu中对这一过程进行实验。最后以hello.s对汇编的结果为例&#xff0c;对汇编代码是如何实现数据的操作&#xff0c;包括常量、变量等&#xff0c;如何实现赋值、算数操作、关系操作和跳转、指针操作和数组操作等进行了详细的说明&#xff0c;更进一步加深了我们对汇编语言的理解。
汇编的概念&#xff1a;汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序(relocatable object program)的格式,并将结果保存在目标文件hello.o中,其中hello.o文件时一个二进制文件。
汇编的功能&#xff1a;将汇编语言的指令转化成机器指令。
在Ubuntu的terminal下输入gcc hello.s -c -o hello.o命令&#xff0c;从而生成hello.o文件
图17 生成hello.o文件
生成hello.o文件&#xff0c;使用readelf查看&#xff1a;
图 18 使用readelf查看
首先用readelf -a hello.o > hello.elf指令生成hello.o的elf格式&#xff1a;
图19 生成hello.o的elf格式
4.3.1 ELF Header
ELF头的开始是一个16字节的魔数&#xff0c;其中前4个字节7f 45 4c 46描述了该文件是否满足ELF格式&#xff0c;第5个字节02描述了该系统架构为x86-64&#xff0c;第6个字节01表示使用小端法&#xff0c;第7个字节01表示ELF的主版本号。
4.3.2 节头部表&#xff08;Section Headers&#xff09;
同样可以使用readelf查看&#xff1a;
图 20 使用readelf查看节头部表部分
可以看到节头部表描述了每个节的名称、类型、地址、偏移量、以及flag、链接、信息和Align等信息
图 21 节头部表其他部分
此外&#xff0c;在节头部表中也提示了在该文件中没有section groups和程序头以及动态节。
4.3.3 重定位节
4.3.3.1 .rela.text
图 22 .text节
这里给出了.text节中需要重定位内容的信息、类型、偏移量和名称等。首先第一个.rodata -4是对.L0所表示的字符串进行重定位&#xff0c;然后是对puts函数、exit函数&#xff0c;.L1所表示的第二个字符串、printf函数、atoi函数、sleep函数、getchar函数进行重定位。
4.3.3.2 .rela.eh_frame
图 23 .eh_frame节
这里的eh_frame是异常处理框架的意思&#xff0c;这里是对一个异常处理单元的重定位。
4.3.4 符号表
图24 符号表部分
最后是符号表部分&#xff0c;根据Bind可以看出这里存放的是程序调用的函数以及所定义的变量的信息。
在Ubuntu下使用objdump -d -r hello.o>Dhello.s对hello.o进行反汇编&#xff0c;将机器语言反汇编为汇编语言&#xff0c;并和hello.s进行比较分析。
图 25 生成反汇编文件Dhello.s
图26 objdump生成的反汇编文件
将其和hello.s比较可以发现共出现了一些不同
很明显&#xff0c;在hello.s中使用的立即数均为十进制&#xff0c;但反汇编的结果中均使用十六进制
图 27 反汇编截图
2.条件分支跳转
图 28 反汇编截图
可以看到&#xff0c;跳转指令由 je .L2变成了一个相对偏倚地址&#xff0c;所以可以推断出在机器语言中并没有对应的.L2的表示&#xff0c;在汇编的过程中将标签全部转化为相对偏移地址。
3.函数调用
在.s程序中函数调用如下所示&#xff1a;
图 29 函数调用反汇编截图
使用的是函数名&#xff0c;但是反汇编的结果使用的全部是main函数的相对偏移地址&#xff0c;同时&#xff0c;每个函数调用都添加了重定位信息&#xff0c;这与.rela.text中的重定位信息一致,说明这些共享库函数通过链接后才能确定函数的确定地址。
图30 函数调用反汇编截图
4.字符串访问变化
图 31 反汇编截图
在hello.s中&#xff0c;字符串的访问使用的是leaq .LC0(%rip),%rdi&#xff0c;而反汇编使用的是&#xff1a;
可以看出对于全局变量的访问&#xff0c;需要通过链接时的重定位确定地址&#xff0c;但是在.s文件中以.LC0表示
本章着重分析了汇编这一过程&#xff0c;首先介绍了汇编的概念和作用是将hello.s程序中对应得汇编语言汇编为hello.o中的机器语言&#xff0c;并在Ubuntu中进行了实验。通过使用readelf查看了ELF文件的内容&#xff0c; 并对它的每一个部分的意思进行了详细的分析。最后使用对hello.o进行反汇编和hello.s进行比较&#xff0c;从而对汇编语言和机器语言的映射关系有了更为清晰的认识。
&#xff08;以下格式自行编排&#xff0c;编辑时删除&#xff09;
链接的概念&#xff1a;链接是将各种代码和数据片段收集并组合成为一个单一的可被加载到内存并执行的文件的过程。链接可以执行于编译时&#xff0c;即源代码被编译成机器代码时&#xff1b;可执行于加载时&#xff0c;即在程序被加载器加载到内存并执行时&#xff1b;可执行于运行时&#xff0c;即由应用程序来执行。在现代系统中&#xff0c;链接是由链接器的程序自动执行。
链接的作用&#xff1a;链接器使分离编译成为可能。可将一个大程序分解为更小&#xff0c;更好管理的模块&#xff0c;可以独立修改和编译这些模块&#xff0c;当改变这些模块中的一个时&#xff0c;只需简单编译&#xff0c;重新链接使用,不必重新编译其他文件。
&#xff08;以下格式自行编排&#xff0c;编辑时删除&#xff09;
在终端输入&#xff1a;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命令&#xff0c;产生hello文件&#xff1a;
图32 生成hello
使用readelf -a hello > helloout.elf生成hello可执行文件的elf格式helloout.elf
使用readelf打开hello文件如下图所示&#xff1a;
图33 ELF
2. 节头部表&#xff1a;
图 34 节头部表
图35 节头部表续
链接后的节头部表记录了各段的基本信息&#xff0c;包括各段的起始地址&#xff0c;偏移量等信息。
3. 程序头
图 36 程序头
程序头部分描述了系统准备程序执行所需的段及其他信息。
使用edb打开hello程序&#xff1a;
图37 edb打开hello程序
其中Data Dump对应的是虚拟地址部分&#xff0c;下面结合5.3分析虚拟地址中一些段的信息。
图38 Data Dump
首先看到地址0x00400000&#xff0c;我们可以看到该部分是ELF文件头部分。
其中第一行16个字节对应了magic序列。
图39 虚拟地址为0x00401000部分
程序的开始部分对应虚拟地址为0x00401000&#xff0c;同节头部表进行对照&#xff0c;该部分对应的是.init段。
由Memory Regions可以看出.init段的终结地址是0x402000&#xff0c;对应得是.rodata的开始地址&#xff0c;.rodata存放的是只读内容&#xff08;r--&#xff09;&#xff0c;对应的是printf中的字符串&#xff0c;相应的&#xff0c;我们可以在虚拟内存中找到该字符串。
图40 Memory Regions
图41 0x402000部分
接下来的地址是0x403000&#xff0c;对应的段是可读可写的&#xff08;rw-&#xff09;&#xff0c;这一部分对应的是运行时堆&#xff0c;同时&#xff0c;全局变量的数组也会存储在这里。
利用objdump -d -r hello > Dhelloout.s生成反汇编文件
在linux的终端下输入objdump -d -r hello&#xff0c;结果如下&#xff1a;
图 42 对hello 的反汇编结果
将其与hello.o进行比较&#xff0c;很容易发现有如下不同&#xff1a;
通过对比可以发现&#xff0c;hello.o的反汇编结果只有.text段即已编译程序的机器代码&#xff0c;
而在hello的反汇编结果中还有.init段、.plt段、.plt.sec段、.fini段。
图43 .init部分
2.增加了一些函数
图44 增加的函数部分
如上图所示&#xff0c;在main函数的上方加入了_start函数
此外在原有的段之外也添加了一些外部库函数&#xff1a;
图45 外部库函数
3.重定位
图46 偏移量相对寻址
在上一章中我们分析函数调用采用的偏移量相对寻址的方法&#xff0c;我们分析&#xff0c;只有经过链接后才可以生成可执行程序&#xff0c;才能够确定执行的地址。如下图所示&#xff0c;callq 25
图47准确寻址
此外&#xff0c;从上图可以看出&#xff0c;跳转指令也由相对寻址变为对准确地址的访问。
使用edb执行hello&#xff0c;说明从加载hello到_start&#xff0c;到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
打开edb运行hello&#xff0c;利用symbols进行查看&#xff0c;可以找到hello运行过程中&#xff0c;调用的子程序如下&#xff1a;
表2 子程序
子程序名 | 地址&#xff08;16进制&#xff09; |
ld-2.31.so!_dl_start | 0x7f92fcd0e100 |
ld-2.31.so!_dl_init | 0x7f92fcd1edf0 |
hello!_start | 0x4010f0 |
libc-2.31.so!__libc_start_main | 0x7f92fcb27fc0 |
hello!printf&#64;plt | 0x4010a0 |
hello!atoi&#64;plt | 4004c0 |
Hello_sleep&#64;plt | 4004f0 |
hello!getchar&#64;plt | 4004d0 |
libc-2.31.so!exit | 7ff339122120 |
hello的所有子程序如下图所示&#xff1a;
图48 hello的所有子程序
&#xff08;以下格式自行编排&#xff0c;编辑时删除&#xff09;
分析hello程序的动态链接项目&#xff0c;通过edb调试&#xff0c;分析在dl_init前后&#xff0c;这些项目的内容变化。要截图标识说明。
动态链接的共享库是一个目标模块&#xff0c;在运行或加载时&#xff0c;可以加载到任意的内存地址&#xff0c;并和一个在内存中的程序相链接起来&#xff0c;该过程由动态链接器执行&#xff0c;这个过程称为动态链接。
因此&#xff0c;系统在调用调用共享库函数时&#xff0c;由于编译器无法预测函数的运行地址&#xff0c;故需增加重定位记录&#xff0c;等待动态链接器处理&#xff0c;链接器采用延迟绑定的策略&#xff0c;从而避免运行时修改调用模块的代码。动态链接器使用过程链接表PLT&#43;全局偏移量表GOT实现函数的动态链接&#xff0c;GOT中存放函数目标地址&#xff0c;PLT使用GOT中地址跳转到目标函数。
所以我们首先找到.got.plt的起始地址为&#xff1a;0x404000
图49 .got.plt的起始地址
然后在虚拟内存中找到这个地址&#xff1a;
图50 dl_init执行之前
在dl_init执行之后&#xff0c;该部分变为&#xff1a;
图51 dl_init执行之后
可以看出&#xff0c;在0x404008位置多了两个地址&#xff0c;分别为0x7fb9e4ff6190和0x7fb9e4fdfa70&#xff0c;这便是在链接之后出现变化的部分。
本章对链接的概念和作用进行了回顾。并在Ubuntu环境下进行实验&#xff0c;首先生成hello可执行文件&#xff0c;然后分析hello的ELF格式、用edb查看hello的虚拟地址空间、将hello反汇编的结果和hello.o的反汇编结果进行比较从而分析链接的重定位过程、对hello的执行流程进行分析&#xff0c;最后对hello的动态链接过程进行分析。通过以上过程&#xff0c;深入学习了从可重定位过程文件到可执行文件的全部过程。
&#xff08;以下格式自行编排&#xff0c;编辑时删除&#xff09;
&#xff08;第5章1分&#xff09;
进程的概念&#xff1a;进程是一个执行中程序的实例&#xff0c;系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据&#xff0c;它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
进行的作用&#xff1a;进程给应用程序提供了两个关键抽象。首先&#xff0c;它提供一个独立的逻辑控制流&#xff0c;提供一个假象&#xff0c;好像程序在独占地使用处理器&#xff1b;其次&#xff0c;它提供了一个私有的地址空间&#xff0c;提供一个假象&#xff0c;好像程序在独占地使用内存系统。
&#xff08;以下格式自行编排&#xff0c;编辑时删除&#xff09;
壳Shell-bash的作用&#xff1a;shell执行一系列的读/求值步骤: 其中读步骤可以读取来自用户的一个命令行&#xff1b;求值步骤负责解析该命令行&#xff0c;并代表用户执行程序。从直观上来看&#xff0c;shell提供了一个界面&#xff0c;用户可以通过该界面对操作系统的内核进行访问。
Shello-bash的处理流程&#xff1a;
&#xff08;以下格式自行编排&#xff0c;编辑时删除&#xff09;
父进程通过fork函数创建一个新的运行的子进程。子进程几乎和父进程相同&#xff0c;可以得到与父进程用户级虚拟地址空间相同但独立的一份副本&#xff0c;包括代码和数据段、堆、共享库和用户栈&#xff0c;以及父进程任何打开文件描述符相同的副本。子进程可以读写父进程中打开的任何文件。子进程和父进程最大的区别在于两者PID不同。
以hello程序为例&#xff0c;在程序所在的文件夹的bash中输入./hello 7203610730 ggt 5&#xff1a;
图52 hello运行
shell会读入输入的命令&#xff0c;调用paeseline函数将输入的命令行按空格切分成一个个参数&#xff0c;进而对读取的命令进行解析&#xff0c;得到以下几个向量argv[0]:”./hello”、argv[1]:”7203610730”、argv[2]:”ggt”和argv[3]:”5”。
然后使用builtin_command函数检查第一个命令行参数是否是一个内置的shell命令&#xff0c;比如quit、pwd等等。由于第一个参数不是shell的内置命令&#xff0c;而是一个可执行目标文件&#xff0c;builtin_comman返回0&#xff0c;所以shell创建一个新的子进程&#xff0c;并在子进程的上下文中加载并运行这个hello文件&#xff0c;并最终得到如上图所示的输出结果。
&#xff08;以下格式自行编排&#xff0c;编辑时删除&#xff09;
execve函数在当前程序的上下文中加载并运行一个新程序&#xff0c;具体的函数为&#xff1a;
图53 execve函数
execve加载并运行可执行文件&#xff08;hello&#xff09;&#xff0c;且带参数列表argv和环境变量列表envp。与fork不同&#xff0c;execve只有在出现错误&#xff0c;例如找不到hello时才会返回到调用程序&#xff0c;所以&#xff0c;exexve调用一次并且从不返回。
当execve加载了hello之后&#xff0c;调用启动代码&#xff0c;启动代码设置栈&#xff0c;并将控制传递给新程序的主函数&#xff0c;该主函数有如下原型&#xff1a;
图54 main函数
exexve函数在当前进程的上下文中加载并运行一个新的程序&#xff0c;它会覆盖当前进程的地址空间&#xff0c;但并没有创建一个新的进程&#xff0c;新的进程仍然有相同的PID&#xff0c;并且纪恒了调用exceve函数时已经打开的所有文件的描述符。
&#xff08;以下格式自行编排&#xff0c;编辑时删除&#xff09;
结合进程上下文信息、进程时间片&#xff0c;阐述进程调度的过程&#xff0c;用户态与核心态转换等等。
前面我们提到过进程可以向每个程序提供一种假象&#xff0c;好似它在独占地使用处理器&#xff0c;我们在用调试器单步执行程序的时候&#xff0c;会看到一系列程序计数器&#xff08;PC&#xff09;的值&#xff0c;这些值对应于可执行目标文件中的指令或者包含在运行时动态链接到程序的共享对象中的指令。这个PC值得序列就叫做逻辑控制流。
处理器的一个物理控制流可被分成多个逻辑流&#xff0c;每个进程一个逻辑流。hello的进程也是其中一个&#xff0c;系统的进程是轮流使用处理器的&#xff0c;每个进程执行它的流的一部分&#xff0c;然后被抢占、轮到其他进程&#xff0c;然后再轮到自己。因此&#xff0c;对于一个运行在这些进程之一的上下文中的程序&#xff0c;看上去就像是再独占地使用处理器&#xff0c;hello进程也是这样。
2.并发流&#xff1a;
一个逻辑流的执行在时间上与另一个流重叠&#xff0c;称为并发流&#xff0c;这两个流被成为并发地运行&#xff0c;用通俗的语言来说&#xff0c;就是说进程B在进程A开始后但结束前开始&#xff0c;即进程A需要为了进程B的运行而暂时挂起&#xff0c;那么称A和B是并发的。
图55 并发流示意图
例如A和B是并发的&#xff0c;但B和C就不是并发的。多个流并发地执行的一般现象称为并发&#xff0c;一个进程例如进程A执行它的控制流的每一个时间段都叫做时间片&#xff0c;多任务也叫做时间分片。而hello程序就有很大可能是有很多个时间分片组成的。
3.hello的私有地址空间
进程提供的另外一个假象就是它好像独占地使用系统地址空间&#xff0c;对于一台n位
地址的机器&#xff0c;地址空间是2n个可能地址的集合&#xff0c;0&#xff0c;1&#xff0c;···&#xff0c;2n-1。但进程也为自己提供有私有地址空间。和这个空间某个地址相关联的内存字节不能被其他进程读或者写&#xff0c;从这个意义来说&#xff0c;这个地址空间是私有的。hello进程也有自己的私有地址空间
4.用户模式和内核模式
处理器通常利用某个控制寄存器的一个模式位来限制一个应用可以执行的指
令以及它可以访问的地址空间的范围&#xff0c;从而保护内核的安全。当设置模式位后&#xff0c;进程运行在内核模式当中&#xff0c;且可以访问内存中的任何位置。没有设置模式位时&#xff0c;进程就运行在用户模式中&#xff0c;该模式不允许进程执行停止处理器、改变模式位等特权指令。进程从用户模式变为内核模式的唯一方法是通过中断、故障或者陷入系统调用这样的异常。
5.上下文切换
上下文切换时内核实现多任务的一种异常控制流。内核为每个进程都维持一个
上下文&#xff0c;上下文是内核重新启动一个被抢占的进程时所需的状态&#xff0c;他由目的寄存器、浮点寄存器、程序计数器、用户栈等的值组成。
当内核抢占当前进程后&#xff0c;就会使用一种称为上下文切换的机制将控制转移到新的进程&#xff0c;上下文切换的步骤分为以下三点&#xff1a;
&#xff08;1&#xff09;保存当前进程的上下文
&#xff08;2&#xff09;恢复某个先前被抢占的进程被保存的上下文
&#xff08;3&#xff09;将控制传递给这个新恢复的进程
&#xff08;以下格式自行编排&#xff0c;编辑时删除&#xff09;
hello执行过程中会出现哪几类异常&#xff0c;会产生哪些信号&#xff0c;又怎么处理的。
程序运行过程中可以按键盘&#xff0c;如不停乱按&#xff0c;包括回车&#xff0c;Ctrl-Z&#xff0c;Ctrl-C等&#xff0c;Ctrl-z后可以运行ps jobs pstree fg kill 等命令&#xff0c;请分别给出各命令及运行结截屏&#xff0c;说明异常与信号的处理。
hello执行过程中出现的异常种类可能会有&#xff1a;中断、陷阱、故障、终止。
中断&#xff1a;中断是来自处理器外部的I/O设备的信号的结果。硬件中断的异常处理程序常常被称为中断处理程序。
陷阱&#xff1a;陷阱是有意的异常&#xff0c;是执行一条指令的结果。就像中断处理程序一样&#xff0c;陷阱处理程序将控制返回到下一条指令。陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口&#xff0c;叫做系统调用。
故障&#xff1a;故障由错误情况引起&#xff0c;它可能能够被故障处理程序修正。当故障发生时&#xff0c;处理器将控制转移给故障处理程序。如果处理程序能够修正这个错误情况&#xff0c;它就将控制返回到引起故障的指令&#xff0c;从而重新执行它。否则处理程序返回到内核中的abort例程&#xff0c;abort例程会终止引起故障的应用程序。
终止&#xff1a;终止是不可恢复的致命错误造成的结果&#xff0c;通常是一些硬件错误&#xff0c;比如DRAM或者SRAM位被损坏时发生的奇偶错误。终止处理程序从不将控制返回给应用程序。处理程序将控制返回给abort例程&#xff0c;abort会终止这个应用程序。
hello正常的执行状态&#xff1a;
图56 hello正常的执行状态
其可以打印八次信息&#xff0c;其中打印的间隔时间由我们决定&#xff0c;当输入任何字符串时结束。
hello运行时按下回车&#xff1a;
图57 hello运行时按下回车
程序继续运行&#xff0c;但由于回车在缓冲区&#xff0c;会出现换行。
按下Ctrl^z&#xff0c;中断程序&#xff1a;
图58 hello运行时按下Ctrl&#43;z
图59 输入ps命令查看进程的情况
这时输入ps命令&#xff0c;查看进程的情况&#xff1a;
可以看到hello进程并没有被回收&#xff0c;PIS为4214
使用job命令进行查看&#xff1a;
图60 用job命令进行查看
使用fg 1 命令可使进程恢复
图61 使用fg 1命令
使用Ctrl^c命令&#xff0c;结束程序
图62 使用Ctrl&#43;c命令和ps命令
正在上传…重新上传取消
此时使用ps命令无法查找到hello进程&#xff0c;说明hello进程已被结束
使用kill命令&#xff1a;
图63 使用kill命令
使用kill命令可将进程杀死
使用pstree命令
图64 使用pstree命令
可以查看父子进程的关系&#xff0c;可以在进程树中找到hello进程
&#xff08;以下格式自行编排&#xff0c;编辑时删除&#xff09;
本章介绍了进程的概念和作用&#xff0c;壳Shell-bash的作用与处理流程&#xff0c;分析了hello的fork进程的创建过程、hello的execve过程&#xff0c;然后从五个方面介绍了hello的进程执行过程&#xff0c;最后介绍了hello的异常与信号处理。本章内容对异常控制流的整个过程进行了回顾&#xff0c;以hello程序为例&#xff0c;进一步加深了我们对进程等概念的理解并在Ubuntu中得到实验验证。
&#xff08;第6章1分&#xff09;
&#xff08;以下格式自行编排&#xff0c;编辑时删除&#xff09;
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
逻辑地址&#xff1a;逻辑地址是指由程序产生的与段相关的偏移地址部分&#xff0c;对应hello.o中的段内偏移地址。
线性地址&#xff1a;线性地址指的是逻辑地址向虚拟地址转换的中间体&#xff0c;逻辑地址在经过段机制之后转化为线性地址&#xff0c;地址空间是一个非负整数地址的有序集合。对hello程序来说&#xff0c;线性地址就是hello产生的段内偏移地址加上相应段的基地址。
虚拟地址&#xff1a;虚拟地址是指程序运行在保护模式之下&#xff0c;这时访问存储器所使用的逻辑地址称为虚拟地址。保护模式中&#xff0c;将程序从磁盘加载进内存的中间加了一个中间层&#xff0c;就是虚拟地址&#xff0c;在程序编译、链接的时候会先映射进入虚拟地址&#xff0c;在运行登封时候会映射进物理地址&#xff0c;这样的好处在于一个进程可以不对其他程序造成影响&#xff0c;起到了进程隔离&#xff0c;保护了其他的进程。
物理地址&#xff1a;计算机的主存是由M个连续的字节大小的单元组成的数组&#xff0c;每个字节都有唯一的物理地址。物理地址位于地址总线上&#xff0c;是以电子形式存在&#xff0c;也就是程序运行时虚拟地址所对应的物理地址。
&#xff08;以下格式自行编排&#xff0c;编辑时删除&#xff09;
段式管理&#xff1a;把一个程序分成若干个段进行存储&#xff0c;每个段都是一个逻辑实体。程序的模块化决定了段的产生。段式管理是通过段表进行的&#xff0c;它包括段号或段名、段起点、装入位、段的长度等。此外还需要主存占用区域表、主存可用区域表。
段内偏移量&#xff1a;是一个不变的常量&#xff0c;用于计算线性地址。
段标识符&#xff1a;由索引号、表指示器和请求者特权级组成。
逻辑地址 &#61; 段标识符&#43;段内偏移量。
索引号是段描述符表的索引&#xff0c;段描述表可细分为全局段描述符表和局部段描述表&#xff0c;这两者的寻找与切换由表指示器确定。
由逻辑地址到线性地址变换时&#xff0c;我们将逻辑地址进行分割成:段选择符&#43;Offset。 看段选择符的TI,如果是0,那么切换到GDT,如果是1,那么切换到LDT。利用段选择符里面的索引号在相应的表里面进行取出段描述符&#xff0c;对段描述符里面的基地址进行取出线性地址即基地址&#43;段内偏移地址。
&#xff08;以下格式自行编排&#xff0c;编辑时删除&#xff09;
首先介绍页的概念&#xff1a;VM系统将虚拟内存分割组成大小固定的块&#xff08;虚拟页VP&#xff09;来实现磁盘到主存的缓存。每个虚拟页的大小为P &#61; 2p字节&#xff1b;类似的&#xff0c;物理内存可被分割为物理页&#xff08;PP&#xff09;&#xff0c;大小也为P字节&#xff0c;物理页也被称为页帧。如下图所示&#xff1a;
图65 VM系统是如何使用主存作为缓存的
图 66 虚拟地址转化为物理地址
接下来&#xff0c;我们来介绍线性地址式如何向物理地址变化的。
线性地址&#xff0c;在这里和虚拟地址相对应&#xff0c;线性地址用两部分来表示&#xff0c;即&#xff1a;
线性地址 &#61; VPN&#xff08;虚拟页号&#xff09;&#43;VPO&#xff08;虚拟页偏移量&#xff09;
虚拟页号在当前进程的CR3寄存器指向当前的页表里面寻找虚拟也&#xff0c;然后把里面所存储的物理页号PPN与物理页偏移量PPO一起返回。
物理地址&#61;PPN&#xff08;物理页号&#xff09;&#43;PPO&#xff08;物理页偏移量&#xff09;
&#xff08;以下格式自行编排&#xff0c;编辑时删除&#xff09;
&#xff08;以下格式自行编排&#xff0c;编辑时删除&#xff09;
&#xff08;以下格式自行编排&#xff0c;编辑时删除&#xff09;
&#xff08;以下格式自行编排&#xff0c;编辑时删除&#xff09;
&#xff08;以下格式自行编排&#xff0c;编辑时删除&#xff09;
&#xff08;以下格式自行编排&#xff0c;编辑时删除&#xff09;
Printf会调用malloc&#xff0c;请简述动态内存管理的基本方法与策略。
&#xff08;以下格式自行编排&#xff0c;编辑时删除&#xff09;
&#xff08;第7章 2分&#xff09;
&#xff08;以下格式自行编排&#xff0c;编辑时删除&#xff09;
设备的模型化&#xff1a;文件
设备管理&#xff1a;unix io接口
&#xff08;以下格式自行编排&#xff0c;编辑时删除&#xff09;
&#xff08;以下格式自行编排&#xff0c;编辑时删除&#xff09;
[转]printf 函数实现的深入剖析 - Pianistx - 博客园
从vsprintf生成显示信息&#xff0c;到write系统函数&#xff0c;到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序&#xff1a;从ASCII到字模库到显示vram&#xff08;存储每一个点的RGB颜色信息&#xff09;。
显示芯片按照刷新频率逐行读取vram&#xff0c;并通过信号线向液晶显示器传输每一个点&#xff08;RGB分量&#xff09;。
&#xff08;以下格式自行编排&#xff0c;编辑时删除&#xff09;
异步异常-键盘中断的处理&#xff1a;键盘中断处理子程序。接受按键扫描码转成ascii码&#xff0c;保存到系统的键盘缓冲区。
getchar等调用read系统函数&#xff0c;通过系统调用读取按键ascii码&#xff0c;直到接受到回车键才返回。
&#xff08;以下格式自行编排&#xff0c;编辑时删除&#xff09;
&#xff08;第8章1分&#xff09;
用计算机系统的语言&#xff0c;逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟&#xff0c;你的创新理念&#xff0c;如新的设计与实现方法。
在C语言学习之初&#xff0c;我曾对计算机是如何编译运行一个hello.c文件感到好奇&#xff0c;还记得陈建文老师用记事本写C语言程序并用命令行实现其运行时我震惊不已&#xff0c;第一次知道C语言程序还能在编译器之外的地方编译。在学习了计算机系统之后&#xff0c;我对一个C语言文本文件是如何在计算机里面运行并生成一个可执行程序有了更加深入的理解。此外&#xff0c;这门课给我最大的收获是我学会了如何从硬件角度去考虑程序的性能&#xff0c;了解了许多计算机硬件的知识&#xff0c;了解到一个优秀的程序员并不能只是一个码农&#xff0c;更应该能和计算机的底层打交道&#xff0c;利用好计算机的底层原理&#xff0c;使我么编写的程序更好更快更强&#xff01;
&#xff08;结论0分&#xff0c;缺失 -1分&#xff0c;根据内容酌情加分&#xff09;
列出所有的中间产物的文件名&#xff0c;并予以说明起作用。
hello.i hello.c经过预处理之后的文件&#xff0c;仍由C语言写成&#xff0c;对源程序进行了一定的补充和修改。
hello.s hello.i经过编译之后的文件,由汇编代码写成
hello.o hello.s经过汇编之后生成的可重定位的目标文件(二进制代码)
hello.elf 用readelf读取hello.o得到的ELF格式信息
Dhello.s 反汇编hello.o得到的反汇编文件
helloout.elf 由hello可执行文件生成的.elf文件&#xff0c;用于和hello.elf做对比
Dhelloout.s 反汇编hello得到的反汇编文件&#xff0c;用于和hello.elf对比
hello hello.o经过链接之后生成的目标文件,可以直接执行
&#xff08;附件0分&#xff0c;缺失 -1分&#xff09;
为完成本次大作业你翻阅的书籍与网站等
[1] CSAPP深入理解计算机系统第三版
[2] CSDN博客 - 专业IT技术发表平台
[3] https://github.com/
&#xff08;参考文献0分&#xff0c;缺失 -1分&#xff09;