计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 未来技术学院
学 号 2022113610
班 级 WL021
学 生 郑劭涵
指 导 教 师 刘宏伟
计算机科学与技术学院
2024年5月
本文以简单的hello程序为出发点,从hello.c源文件变成hello可执行程序所经历的过程,以及hello可执行程序的执行过程切入,详细讲述了一个程序从Program到Progress,从zero到zero的整个过程。短短的一个hello程序,需要经历预处理、编译、汇编、链接才能得到hello这个可执行程序,得到可执行程序后,又经过shell创建子进程、execve加载程序,最后再被父进程回收,至此,hello的生命结束。
关键词:预处理;编译;汇编;链接;进程;储存
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第1章 概述
1.1 Hello简介
P2P:From Program to Process
描述从代码一个文本文件的项目变成一个进程,原本的program,即hello.c源文件经过cpp预处理生成hello.i,再经过cc1编译生成hello.s汇编语言程序,之后再由as生成 hello.o二进制的可重定位目标程序,最后由连接器ld链接生成二进制的可执行目标程序 hello,这个目标程序运行起来就得到我们的progress,如图所示。
020:From Zero-0 to Zer0-0
讲述了运行一个进程的过程,从没有这个进程到创建进程,再到最后进程终止之后被回收。如果要运行一个进程,首先判断是否是系统内置命令,如果是,则直接执行,如果不是,fork生成子进程,为进程调用execve,开始执行,结束后由父进程或者养父进程回收,释放内存空间,重现变成“0”。
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
1.2 环境与工具
硬件环境
X64 CPU;2.80GHz;16G RAM;512G SSD
软件环境
Windows 11 64位;VMWare Workstation Pro 17;Ubuntu 22.04.2;LTS 64位
开发工具
Visual Studio 2022 64位;Visual Studio Code 64位;CodeBlocks 32位;gedit+gcc
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
1.3 中间结果
文件名 | 文件作用 |
hello.c | 源文件 |
hello.i | hello.c经过预处理(cpp)后的文本文件 |
hello.s | hello.i经过编译(ccl)得到的汇编文件 |
hello.o | hello.s经过汇编(as)得到的可重定位目标文件 |
hello | hello.o与其他目标文件链接后得到的可执行目标文件 |
elf.txt | hello.o的elf文件 |
elf2.txt | hello的elf文件 |
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
1.4 本章小结
本章讲述了hello程序p2p,020的过程,从程序到进程的过程,还有一个程序的生命周期,还对我的环境与工具进行了介绍,最后,列举了在调试hello过程中的中间结果,和它们的作用。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
概念:预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。这个过程由预处理器对程序源代码文本进行处理,把源代码分割或处理成为特定的单位(在C/C++的术语中称为“预处理记号”),得到的结果再由编译器核心进一步编译。这个过程并不对程序的源代码进行解析。
作用:
- 宏定义:宏定义是用一个标识符来表示一个字符串,这个字符串可以是常量、变量或表达式。在宏调用中将用该字符串代换宏名,方便在源代码中进行统一替换或修改。
- 文件包含:在一个文件中包含另一个文件的全部内容,预处理过程中就将被包含文件中的内容去替换预处理指令;
- 条件编译:条件编译允许只编译源程序中满足条件的程序段,使生成的目标程序较短,从而减少了内存的开销并提高了程序的效率。
2.2在Ubuntu下预处理的命令
在程序所在终端输入gcc -E hello.c -o hello.i得到预处理文件后如下
图1预处理后文件
2.3 Hello的预处理结果解析
打开hello.i文件观察三个#include的变化,可以看到注释已经被删除,文件扩展为3092行,但是主体代码只有14行。
图3 hello.i文件
搜索三个.h文件可以看到其调用路径已经被预处理器写入.i文件,说明#include<stdio.h>、#include<stdlib.h>、#include<unistd.h>文件已经被预处理。
图4 文件包含stdio预处理
图5 文件包含unistd预处理
图6 文件包含stdlib预处理
图7 文件起始部分
由图7可以看出,开始部分有一系列外部库.h文件路径。
图8 文件typedef部分
由图8可知,文件中还有一些typedef,将头文件所用到的别名对应到标准数据类型当中。
图9 文件内部函数声明部分
由图9可知,还有些关于内部函数的声明。
图10 文件main函数部分
由图10可知,最后才是我们的main函数部分。
2.4 本章小结
本章介绍了预处理的概念和作用,指出预处理指令,对hello.c源文件进行了预处理,并对预处理结果进行了分析。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
概念:编译是指编译器将源程序转换为计算机可以识别的机器语言——汇编语言,编译可以分为分析和整合两部分,分析过程将源程序分成多个结构,校验其格式,收集源程序信息,并将其放在符号表中;整合过程根据分析过程传递的信息构造目标程序。最后生成hello.s文件。
作用:编译分为六个步骤,每个步骤分别有不同的作用
- 词法分析:词法分析器读取源程序的字符流并对其进行扫描,将其组成有意义的词素序列,传递给语法分析。
- 语法分析:语法分析其用词法单元的第一个分量来创建语法树,树中的每个非叶结点都表示一个运算,左右结点表示运算分量。
- 语义分析:语义分析器语法树和符号表中的信息来检查源程序是否和语言定义的语义一致。语义分析器也收集类型信息,以便后续的中间代码生成器使用。
- 中间代码生成:生成一个明确的低级类机器语言的中间表示。
- 代码优化:生成效率更高,更好的目标代码
- 将生成的中间代码映射为机器代码,为每个变量分配寄存器或内存位置,并且将中间指令翻译成机器指令序列。
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
3.2 在Ubuntu下编译的命令
在程序所在终端输入gcc -S hello.i -o hello.s得到预处理文件后如下
图11生成编译文件
3.3 Hello的编译结果解析
3.3.1数据保存:
在函数中,常量包括printf函数的字符串,if(argc!=5)中的常数5和for(i=0;i<10;i++)中常数10。
图12 字符串常量
由图12可知,系统会将printf打印的字符串常量存到.LC0中。
图13 常量5存入栈
由图13可知,cmpl比较argc与5是否相等,如果不相等,就上述的字符串常量地址加载到寄存器中。
图14 常量10操作,存入9作为比对小于10的数
3.3.2变量存储:
hello.c文件不存在全局变量,存在局部变量i,指针数组argv[]和局部变量argc,
均保存在栈中:
图15 局部变量argv、argc保存
在图15中可以看到栈指针%rsp减32创造栈空间,第一个参数argc保存在%rbp-20地址处,第二个参数argv保存在地址%rbp-32处
图16 局部变量i保存
由图16可知,局部变量i保存在%rbp-4地址处,并赋初值0。
3.3.3算术操作:
图17 i++算数操作
在for循环中i每次循环加1,addl指令将%rbp-4地址对应内存处的值加一,等价于i++。
3.3.4条件判断和exit函数调用:
图18 if条件语句
If判断argc值,若argc不等于4,打印字符串并结束进程
图19 对应汇编语言
cmpl指令将%rbp-20地址处保存的argc与立即数5比较,若相等,跳转到L2(如图21);若不相等将.LC0保存的字符串打印,并调用exit函数。
3.3.5控制转移和getchar函数调用:
图20 在if判断argc为5后进行for循环
图21 设置i的初值后跳转到L3
如图21,在L2中将i设为0后跳转到L3。
图22 for循环判断
如图22,在L3中jle指令判断i与9的关系,若i小于等于9,跳转到L4(如图23),否则调用getchar函数,最后用ret指令返回0;
3.3.6函数调用和数组操作:
在for循环内部调用了printf函数,sleep函数,atoi函数
图23 for循环内部
调用printf函数时%rdi保存第一个参数字符串.LC1;%rsi保存第二个参数argv[1];%rdx保存第三个参数argv[2];%rcx保存第四个参数argv[3](他们均由栈指针%rax从栈中赋值)。
调用sleep函数时%rax+32从栈中获取参数argv[4]并赋给%rdi.
调用函数atoi将字符串转换为int类型,最后调用sleep函数。
此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析。
3.4 本章小结
本章解析hello.s文件,对照源文件一步一步解析汇编指令,分析数据储存,变量保存,函数调用,控制转移,数组操作等如何用汇编语言一步步实现,同时分析了汇编操作中寄存器和栈的变化。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
概念:汇编器将.s汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在.o目标文件中。.o文件是一个二进制文件,其包含程序的指令编码。
作用:汇编器首先检查汇编程序语法的正确性,若正确,则将其翻译成与之等价的机器语言指令,并把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件中。
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
4.2 在Ubuntu下汇编的命令
Gcc -c hello.s -o hello.o
图24 汇编命令
应截图,展示汇编过程!
4.3 可重定位目标elf格式
用readelf命令查看hello.o的elf文件:
图25 查看elf文件
4.3.1 ELF头
图26 ELF头
ELF头以16字节的magic(魔数)序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序,elf头文件剩下的部分包含帮助连接器语法分析和解释目标文件的信息,其中包括elf头的大小、目标文件的类型、机器类型、字节头部表的文件偏移,以及节头部表中条目的大小和数量等信息。
4.3.2节头部表
使用readelf -S hello.o 命令查看节头部表
图27 节头表
图28:elf可重定位目标文件格式
节头部表描述了hello.o中各个节的语义,包括节的类型、位置和大小等信息。
Key to flags 描述了各节的读写权限。
如上图27、28,解析来的节头表描述了不同节的位置和大小:
1、.text:已编译程序的机器代码。
2、.rodata:只读数据。
3、.data:已初始化的全局变量和静态C变量。
4、.bss:未初始化的全局变量和静态C变量,仅是占位符,不占据任何实际磁盘空间。
5、.symtab:符号表,存放程序中定义和引用的函数和全局变量信息,不包括局部变量的条目。
6、.rel.text:.text节的重定位信息,用于重新修改代码段的指令中的地址信息。
7、.rel.data:.data节的重定位信息,用于对被模块使用或定义的全局变量重定位的信息。
8、.debug:调试符号表,只有以-g方式调用编译器驱动程序时,才会得到这张表。
9、.line:原始C源程序中的行号和.text节中机器指令之间的映射。
10、.strtab节:字符串表,包括.symtab和.debug节中的符号表。
4.3.3重定位条目
使用 readelf -r hello.o 命令查看hello.o的重定位条目
图29 重定位节
当汇编器生成hello.o后,它并不知道数据和代码最终将存放在内存中的什么位置,它也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。所以,无论何时汇编器遇到对最终位置未知的目标引用,他就会生成一个重定位条目,告诉连接器在将目标文件合并成可执行文件时,如何修改这个引用。
重定位节是一个text节中位置的列表,包含text节中需要进行重定位的信息,用于在链接是修改指令中的地址。
4.3.4符号表
使用readelf -s hello.o命令查看.symtab节中的ELF符号表
图30 符号表
如图30,符号表存放了hello函数用到的符号信息
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
4.4 Hello.o的结果解析
图31反汇编文件
4.4.1进制改变
图32 hello.s与反汇编文件中数字的对比
如图32,在汇编文件(hello.s)中栈指针减32,在反汇编文件中栈指针减0x20表示为十六进制。
4.4.2分支转移
图33 hello.s与反汇编文件中分支转移的对比
如图33,可以发现分支跳转和函数调用不一样,在汇编文件(hello.s)中,分支跳转的目标位置是通过.L1 .L2这样的助记符来实现,而反汇编文件中,跳转的目标位置是指令的位置。
4.4.3函数调用
图34 hello.s与反汇编文件中函数调用的对比
函数调用在汇编文件(hello.s)中,call后的目标函数是它的函数名,而在反汇编文件中,call的是目标函数的相对偏移的值。
4.5 本章小结
本章分析了汇编的过程,并分析了ELF头、节头部表、符号表以及重定位节。表交了hello .s和hello.o反汇编之后的代码的不同。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
概念:将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载到内存并且执行。
作用:可以是程序分离为各个模块,然后整合在一起执行,因此方便了后续对程序各个模块的分离修改和编译,这使得程序更加易于管理。
注意:这儿的链接是指从 hello.o 到hello生成过程。
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
以此生成hello可执行文件
图35 链接指令生成可执行文件
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
5.3 可执行目标文件hello的格式
用readelf查看hello可执行文件hello的ELF文件:
图36 查看elf文件
5.3.1 ELF头
图37 hello程序的ELF文件的ELF头
与hello.o的ELF文件相比,入口地址由0x0变为了0x4010f0,程序的节和起始地址和大小变化,多了13个节
5.3.2 节头部表
图38 节头表
连接器将各个文件对应的段都合并了,并且重新分配计算了相应的节的类型、位置大小等信息。
各个节的地址从0开始进行了分配,可以看到.text节的起始地址是0x4010f0,这刚好就是前面elf头的程序入口地址,与.text中存放程序的机器代码符合。
5.3.3 重定位节
图39 重定位节
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
5.4 hello的虚拟地址空间
图40 edb查看hello文件的虚拟内存
对比ELF节内容
图41 .init的位置
在ELF节头中.init显示在401000地址位置在虚拟内存中对应:
图42.init在虚拟内存中的内容
又例如,对比.text文件
图43 .text的位置
在ELF节头中.text显示在4010f0地址位置在虚拟内存中对应:
图44.text在虚拟内存中的内容
依次可找到其他节头在虚拟内存中的位置
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
5.5 链接的重定位过程分析
图45 hello程序反汇编
5.5.1 新增函数
如图,链接以后,加入了许多用到的的库函数:
图46 链接的函数
5.5.2 新增节
如图,新增了.init节和.plt节
图47 .init节
图48 .plt节
5.5.3 函数调用与跳转
由于hello文件已经是重定位后的可执行目标文件,所以每一个call/jmp语句的目标地址就是确切的虚拟地址。
图49 main函数
链接过程:
在符号解析完成后,每个符号定义都与符号引用相关联。先将每个同类型的数据重新定位到相应的节上,然后再对每个节赋予不同的地址,节中的数据再依据它相对于节的偏移量找到自己对应的位置。
如何重定位:
用节头的位置加上需要重定位的项目相对于节头的偏移量
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出入口点,也就是 _start 函数的地址。这个函数是在系统目标文件 ctrl.o 中定义的。_start 函数调用系统启动函数 __libc_start_main,该函数定义在 libc.so 中。它初始化执行环境,调用用户层的 main 函数,处理 main 函数的返回值,并且在需要的时候把控制返回给内核。
图50 在edb中执行hello
hello!_init <0x0000000000401000>
hello!puts@plt <0x0000000000401030>
hello!printf@plt <0x0000000000401040>
hello!getchar@plt <0x0000000000401050>
hello!atoi@plt <0x0000000000401060>
hello!exit@plt <0x0000000000401070>
hello!sleep@plt <0x0000000000401080>
hello!_start <0x00000000004010f0>
hello!_dl_relocate_static_pie <0x0000000000401120>
hello!main <0x0000000000401125>
hello!_fini <0x0000000000401238>
使用gdb/edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程(主要函数)。请列出其调用与跳转的各个子程序名或程序地址。
5.7 Hello的动态链接分析
动态的链接器在正常工作时链接器采取了延迟绑定的链接器策略,将过程地址的绑定推迟到第一次调用该过程时。
GOT表中存放着函数的目标地址,PLT表则使用GOT中的地址来跳转到目标函数.首先找到GOT地址
图51 ELF文件中.got地址
图52 运行dl_init前
图53 dl_init后
可以由以上两图很清楚的发现在00403ff0时候发生了变换。
分析hello程序的动态链接项目,通过edb/gdb调试,分析在动态链接前后,这些项目的内容变化。要截图标识说明。
5.8 本章小结
本章介绍了链接的概念和作用,详细说明了链接的过程,可执行文件hello的ELF头和可执行文件hello的反汇编过程,对链接的重定位过程进行了分析,对hello的动态链接过程进行了分析。(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程(Process)是计算机中已分配资源的一个正在运行的程序的实例。在操作系统中,进程是系统进行资源分配和调度的基本单位,是操作系统结构的基础。从本质上说,进程是处于执行中的程序,包括程序计数器、寄存器和变量的当前值。
作用:hello 在运行时,操作系统会提供一种假象,就好像系统上只有这个程序在运行。程序看上去是独占地使用处理器、主存和 I/O 设备。处理器看上去就像在不间断地一条接一条地执行程序中的指令,即该程序的代码和数据是系统内存中唯一的对象。这些假象就是通过进程来实现的。并且使得多个程序可以并发执行、共享资源,提高了系统的效率和资源利用率。同时,进程也为程序的编写和运行提供了更加灵活和方便的方式。
6.2 简述壳Shell-bash的作用与处理流程
作用:Shell-bash是Linux系统下的一个命令行解释器,也是用户和操作系统内核之间的接口。它的主要作用是将用户的指令翻译给操作系统内核,让内核来进行处理,并把处理的结果反馈给用户。具体来说,用户可以通过bash shell来启动、挂起、停止甚至编写一些程序。Shell的存在使得用户不会直接操作OS,保证了OS的安全性。
处理流程:
1.在 Shell 中输入 hello 程序的路径。
2.Shell 判断用户输入的是否为内置命令,如果不是,就认为它是一个可执行目标文件。
3.Shell 构造 argv 和 envp。
4.Shell 使用 fork() 创建子进程,调用 execve() 函数在新创建的子基础南横的上下文中加载并运行 hello 程序。将 hello 中的 .text 节、.data 节、.bss 节等内容加载到当前进程的虚拟地址空间。
5.execve() 函数调用加载器,跳转到程序的入口点,开始执行 _start 函数,我们的 hello 程序便正式开始执行了。
6.3 Hello的fork进程创建过程
使用fork()创建子进程之后,子进程与父进程映射到相同的地址空间,但拥有私有的写时复制区域,且父子进程的pid不同,fork函数调用一次返回两次,子进程返回0,父进程返回子进程pid,在此处hello程序中,shell是父进程,而我们要执行的hello是子进程。
6.4 Hello的execve过程
Hello进程创建后调用execve函数,在当前进程的上下文中加载并运行一个新程序。execve函数加载并运行可执行目标文件hello,且带参数列表argv和环境变量envp。只有当出现错误时,例如找不到filename,execve才会返回到调用程序,调用成功不会返回。与fork不同,fork一次调用两次返回,execve一次调用从不返回。
6.5 Hello的进程执行
6.5.1.逻辑控制流
操作系统将一个 CPU 物理控制流,分成多个逻辑控制流,每个进程独占一个逻辑控制流。当一个逻辑控制流执行的时候,其他的逻辑控制流可能会临时暂停执行。一般来说,每个逻辑控制流都是独立的。当两个逻辑控制流在时间上发生重叠,我们说是并行的。处理器在多个进程中来回切换称为多任务,每个时间当处理器执行一段控制流称为时间片。因此多任务也指时间分片。
6.5.2 用户模式与内核模式
处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中, 用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任 何命令,并且可以访问系统中的任何内存位置。上下文切换的时候,进程就处于内核模式。
6.5.3 上下文切换
操作系统内核为每个进程维护一个上下文。所谓上下文就是内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成,这些对象包括通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表,包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。
图54 上下文切换
6.5.4 hello的执行
以hello作为一个独立的进程与其他进程并发执行,内核为hello维持一个上下文,在hello的某个时间片内,若内核判断它已经运行了足够长的时间,那么内核可以决定抢占hello进程,并重新开始一个之前被抢占了的进程,并使用上下文切换的机制将控制转移到新的进程,该机制具体执行分为三步:1)保存当前进程的上下文,2)恢复被抢占进程被保存的上下文,3)将控制转移给这个新的进程;这样,内核就完成了对hello与其他进程的调度。
6.6 hello的异常与信号处理
6.6.1 异常
异常可以分为四类:中断、陷阱、故障和终止。它们的性质如图:
图55 异常类别及其性质
中断:比如在 hello 运行过程中,我们敲击键盘,那么就会触发中断,系统调用内核中的中断处理程序执行,然后返回,hello 继续执行,如图:
图56中断处理
陷阱:陷阱就是系统调用,我们的 hello 运行在用户模式下,无法直接运行内核中的程序,比如像 fork,exit 这样的系统调用。于是就通过陷阱的方式,执行 systemcall 指令,内核调用陷阱处理程序来执行系统调用,如图:
图57陷阱
故障:当我们的 hello 运行时,当某一条指令引用一个虚拟地址,而地址相对应的物理页面不在内存中,就会发生故障。内核调用故障处理程序(这里是缺页处理程序),缺页处理程序从磁盘中加载适当的页面,然后将控制返回给引起故障的指令,该指令就能顺畅地执行了。
图58故障
终止:hello 在运行时,也有可能遇到硬件错误,那就只能自认倒霉,终止 hello 的运行。
图59 终止
6.6.2信号
在运行hello过程中,测试部分信号。
图60 正常运行
Sigstp:在hello在前台运行时,按下ctrl + z 会向它发送sigstp信号,这个进程就会暂时挂起,可以使用fg %<pid>命令,让它在前台继续执行:
图61 输入Ctrl-Z
图62 输入fg%<pid>
Sigint:当hello在前台运行时,按下ctrl + c 会向它发送sigint信号,这个进程就会被终止:
图63 输入Ctrl-C
Ctrl-z后运行ps jobs pstree kill 等命令:
图64 运行pstree命令
图65 运行ps、jobs、fg、kill命令
输入ps查看当前进程的状态包括PID
输入jobs查看被挂起的进程hello
输入 kill -9 和PID终止进程
再输入ps发现hello已经被杀死
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
6.7本章小结
本讲解了 hello 如何运行在操作系统的上下文中,以及它如何受到信号的控制。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址空间:汇编代码中的地址,由段寄存器:偏移量构成。
线性地址空间:逻辑地址经过转换、计算后得到的非负整数地址的序列,线性地址空间就是指这个地址序列的集合。
虚拟地址空间:与线性地址空间等价。
物理地址空间:数据存储在的真实地址。
Hello中汇编代码里面出现的左侧的数据应该是线性地址
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
7.2 Intel逻辑地址到线性地址的变换-段式管理
7.2.1逻辑地址的组成
逻辑地址由两部分组成:段选择符(Segment Selector)和偏移量(Offset)。段选择符是一个16位的字段,用于从段描述符表(如全局描述符表GDT或局部描述符表LDT)中选择一个段描述符。偏移量则指明了从段开始的地方到实际地址之间的距离。
7.2.2段描述符表
全局描述符表(GDT)和局部描述符表(LDT):段描述符被存储在GDT或LDT中。这些表包含了关于各个内存段的详细信息,如段的基地址、长度和访问权限等。
段描述符:每个段描述符通常包含8个字节(64位),包含了关于该段的详细信息,如Base字段(描述段的开始位置的线性地址)等。
7.2.3变换过程
选择段描述符表:通过检查段选择符的TI字段来决定从GDT还是LDT中获取段描述符。TI=0表示从GDT中获取,TI=1表示从LDT中获取。
计算段描述符地址:使用段选择符的index字段(前13位)来计算段描述符在描述符表中的地址。通常,这个地址是通过将index字段的值乘以8(因为每个段描述符是8字节长)并与gdtr或ldtr寄存器的内容相加得到的。
获取段描述符:根据计算出的地址从GDT或LDT中获取相应的段描述符。
计算线性地址:将逻辑地址的偏移量与段描述符的Base字段的值相加,得到最终的线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
7.3.1页式管理的基本概念:
页式管理是一种内存空间存储管理的技术,它将各进程的虚拟空间(也称为线性地址空间)划分成若干个长度相等的页(page)。同时,页式管理把内存空间也按页的大小划分成片或者页面(page frame)。
7.3.2建立页表:
页式管理为每个进程建立了一个或多个页表(Page Table),用于建立页式虚拟地址与内存地址之间的一一对应关系。页表中的每个条目(Page Table Entry, PTE)都包含了一个虚拟页对应的物理页框的基地址和其他相关信息。
7.3.3线性地址到物理地址的变换过程:
当CPU需要访问某个线性地址时,它会首先查看页表来找到对应的物理地址。
具体来说,CPU会提取线性地址中的页号和页内偏移量。页号用于在页表中找到对应的页表条目(PTE),而页内偏移量则保持不变,因为它表示的是页内的相对位置。
一旦找到了PTE,CPU就会从PTE中读取物理页框的基地址,并将其与页内偏移量相加,从而得到最终的物理地址。
7.3.4页表的位置和访问:
页表通常存储在内存中,但也可以被缓存到CPU的缓存中以提高访问速度。
CPU使用专门的硬件结构(如页表缓存TLB)来加速页表的访问过程。当TLB中不存在所需的页表条目时,CPU会访问内存中的页表来获取所需的信息。
7.3.5请求页式管理和预调入页式管理:
动态页式管理是在静态页式管理的基础上发展起来的,它分为请求页式管理和预调入页式管理。
请求页式管理是在需要执行某条指令或访问某个数据时发现它们不在内存中时,触发缺页中断,并将相应的页面从外存调入内存。
预调入页式管理则是系统根据一定的算法预测哪些页面可能会被访问,并提前将它们从外存调入内存。
7.3.6数字信息:
在32位系统中,线性地址通常是一个32位无符号整数,可以表示高达4GB的地址空间。
页的大小可以根据系统配置的不同而变化,但常见的页大小有4KB、8KB等。
页表的大小也取决于系统的配置和进程的大小,每个进程至少有一个页表。
7.4 TLB与四级页表支持下的VA到PA的变换
页表技术虽然能让我们再给出虚拟地址的时候,很大概率通过查找页表来找到内存地址,但是查页表也是访问内存的过程,也很浪费时间。利用局部性原理,像缓存一样,将最近使用过的页表项专门缓存起来。因此出现了TLB(后备转换缓冲器,也叫快表),之后找页表项的时候,先从快表找,找不到在访问内存中的页表项。同理,四级页表能保证页表项的数量少一些。
图66 地址翻译概况
7.5 三级Cache支持下的物理内存访问
得到物理地址后,将物理地址分为 CT(标记位)、CI(组索引) 和 CO(块偏移)。根据 CI 查找 L1 缓存中的组,依次与组中每一行的数据比较,有效位有效且标记位一致则命中。如果命中,直接返回想要的数据。如果不命中,就依次去 L2、L3 缓存判断是否命中,命中时将数据传给 CPU 同时更新各级缓存。
7.6 hello进程fork时的内存映射
在shell中输入命令./hello后,内核调用fork函数创建子进程,为hello程序的运行创建上下文,并分配一个与父进程不同的唯一的PID。为了给子进程创建虚拟内存,创建了当前进程的 mm_struct、区域结构和页表的原样副本。将这两个进程的每个页面都标记为只 读,并将两个进程中的每个区域结构都标记为私有的写时复制。
7.7 hello进程execve时的内存映射
execve() 函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件 hello 中的程序,用 hello 程序有效地替代了当前程序。加载并运行 hello 需要以下几个步骤:
- 1.删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构。
- 2.映射私有区域,为新程序的代码、数据、bss 和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为 hello 文件中的 .text 和 .data 区,bss 区域是请求二进制零的,映射到匿名文件,其大小包含在 hello 中,栈和堆地址也是请求二进制零的,初始长度为零。
- 3.映射共享区域, hello 程序与共享对象 libc.so 链接,libc.so 是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
- 4.设置程序计数器(PC),execv() 做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
当hello进程执行execve系统调用时,会进行一系列的内存映射操作,以确保新的程序能够正确地在当前进程的地址空间中运行。以下是execve时内存映射的简述:
7.7.1创建新的进程映像:
execve会创建一个新的进程映像,用于加载并执行新的程序。这个新的映像会替代当前进程的映像。
7.7.2销毁旧内存映像:
在加载新程序之前,execve会销毁当前进程的所有内存映像,包括代码、数据、堆、栈等。这是为了确保新的程序能够在一个干净的环境中运行。
7.7.3映射可执行文件:
execve会将新的可执行文件(例如hello)映射到进程的地址空间中。这通常是通过读取可执行文件的各个段(如代码段、数据段等),并将它们映射到进程的虚拟地址空间的连续区域来实现的。
7.7.4文件映射与物理内存:
对于可执行文件的映射,操作系统会采用按需页面调度的策略。这意味着只有当CPU第一次引用某个页面时,操作系统才会将该页面从磁盘加载到物理内存中。在此之前,这些页面只是存在于进程的虚拟地址空间中,没有实际占用物理内存。
7.7.5内存布局:
在新的进程映像中,代码段(包含程序的指令)通常会被映射到低地址空间,而数据段(包含全局变量和静态变量)会被映射到高地址空间。此外,还会为堆和栈分配额外的内存空间。
7.7.6页表更新:
为了实现虚拟地址到物理地址的映射,操作系统会更新进程的页表。页表是一个数据结构,用于记录虚拟地址页面与物理地址页面之间的对应关系。当执行execve时,操作系统会修改页表以反映新的内存布局。
7.7.7执行新程序:
一旦内存映像被创建并映射到进程的地址空间中,并且页表被更新,CPU就可以开始执行新程序了。CPU会根据页表将虚拟地址转换为物理地址,并从物理内存中读取指令和数据来执行。
7.7.8资源释放:
如果execve成功执行,那么原始进程的所有资源(如文件描述符、信号处理程序等)都会被释放或重置,以确保新的程序能够在一个干净的环境中运行。
7.8 缺页故障与缺页中断处理
缺页其实就是DRAM缓存未命中。当我们的指令中对取出一个虚拟地址时,若我们发现对该页的内存访问是合法的,而找对应的页表项式发现有效位为0,则说明该页并没有保存在主存中,出现了缺页故障。
此时进程暂停执行,内核会选择一个主存中的一个牺牲页面,如果该页面是其他进程或者这个进程本身页表项,则将这个页表对应的有效位改为0,同时把需要的页存入主存中的一个位置,并在该页表项储存相应的信息,将有效位置为1。然后进程重新执行这条语句,此时MMU就可以正常翻译这个虚拟地址了。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高地址)。对于每个进程,内核维护着一个变量brk(读作“break”),它指向堆的顶部。
分配器有两种基本风格:
(1)显式分配器:要求应用显式地释放任何分配的块,例如C标准库提供的malloc程序包。
(2)隐式分配器:要求分配器检测一个已分配块何时不再被程序所使用,那么就是放这个块,也被称为垃圾收集器。
分配器简单来说有以下几种实现方式:
图67 分配器实现方式
Printf会调用malloc,请简述动态内存管理的基本方法与策略。
7.10本章小结
本章介绍了hello的存储器地址空间的相关概念。详细讲解了Intel的段式管理和页式管理方式。并且说明了程序运行时利用四级页表将虚拟地址VA转换成物理地址PA的过程和使用三级Cache访存的过程。本章还介绍了在调用fork函数创建子进程和调用execve函数加载并运行程序时的内存映射情况。针对访存时可能引发的缺页故障,我们介绍了缺页中断处理机制,讨论了成功的处理过程和失败的处理过程。最后,还对动态存储分配管理的基本方法与各种策略进行了介绍。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
一个Linux文件就是一个m字节的序列:B0 , B1 , … , Bk , … , Bm-1。
所有的 IO 设备(例如网络、磁盘、终端)都被模型化为文件,而所有的输入和输出都被 当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单低级的应用接口,称为 Unix I/O,这使得所有的输入和输出都被当做相应文件的读和写来执行
8.2 简述Unix IO接口及其函数
打开文件:返回一个小的非负整数,即描述符。用描述符来标识文件。
改变当前文件位置 从文件开头起始的字节偏移量。系统内核保持一个文件位置k,对于每个打开的文件,起始值为0。
读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。写操作就是从内存复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
关闭文件:当应用完成了对文件的访问之后,它就通知内核关闭这个文件。
函数:
Open()-打开一个已经存在的文件或是创建一个新文件
Read()-从文件读取数据,执行输出
Write()-从文件中读取数据,执行输出
Close()-关闭一个被打开的文件
Lseek()-用于在指定的文件描述符中将文件指针定位到相应位置
8.3 printf的实现分析
printf函数的函数体:
图68 printf函数的函数体
主体部分,第一行目的是让argv指向第一个字符串;第二句的作用是格式化,并返回要打印的字符串的长度,第三句的作用是调用write函数将buf的前i个字符输出到终端,调用了unix I/O。
官方解释:
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等。字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
https://www.cnblogs.com/pianist/p/3315801.html
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
getchar函数体:
图69 getchar源代码
getchar函数内部调用了read函数,通过系统调用read读取存储在键盘缓冲区的ASCII码,直到读到回车符才返回。不过read函数每次会把所有的内容读进缓冲区,如果缓冲区本来非空,则不会调用read函数,而是简简单单的返回缓冲区中最前面的元素。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章主要介绍了 Linux 的 IO 设备管理方法、Unix IO 接口及其函数,深入分析了 printf 函数和 getchar 函数的实现。(第8章1分)
结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
结论:最开始,hello.c待在磁盘里,等待被执行。终于,hello.c经过了预处理,头文件被引入、宏被展开、注释被删除等,变成了hello.i文件。接着经过编译器的处理,变成了一条条CPU可以执行的汇编代码hello.s,紧接着经过汇编器的转化,变成了只能被机器识别的0/1二进制机器代码hello.o,最后经过链接器的链接,变成了可执行文件hello。
可执行文件成功生成后,为了执行它,程序员在终端输入./hello。虽然结果瞬间就出来了,但是中间还是经历了很多。
shell解析命令行输入的命令,然后调用fork创建子进程,并用execve映射到虚拟内存中。当CPU执行到hello时,开始读取对应的虚拟内存地址,通过缺页异常将hello放入主存中。之后通过四级页表、一层层缓存……终于hello被加载到了处理器内部。
然后,再通过I/O包装的I/O函数,终于结果被输出到终端。
最后hello.c程序被回收,重新进入硬盘……
虽然hello的一生如此短暂,但是却坎坷而精彩!
感悟:Hello world作为我最初接触计算机方面知识的时候第一个成功实现出来的程序,对我有非凡的意义,在经历了一学期对计算机系统这门课的学习后,我发现表面上如此简单的程序,其背后的实现过程竟是如此的庞大且复杂,但同时它的一生也是完整的、精妙的,让我领略到计算机系统的魅力。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
文件名 | 文件作用 |
hello.c | 源文件 |
hello.i | hello.c经过预处理(cpp)后的文本文件 |
hello.s | hello.i经过编译(ccl)得到的汇编文件 |
hello.o | hello.s经过汇编(as)得到的可重定位目标文件 |
hello | hello.o与其他目标文件链接后得到的可执行目标文件 |
elf.txt | hello.o的elf文件 |
elf2.txt | hello的elf文件 |
列出所有的中间产物的文件名,并予以说明起作用。
(附件0分,缺失 -1分)
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1] 林来兴. 空间控制技术[M]. 北京:中国宇航出版社,1992:25-42.
[2] 辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.
[3] 赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).
[4] 谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.
[5] KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.
[6] CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.
(参考文献0分,缺失 -1分)