计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术
学 号 202xxxx
班 级 2xxxx
学 生 豪仔
指 导 教 师 刘宏伟
计算机科学与技术学院
2022年5月
本文以hello程序在linux系统中从编译到运行到回收的全过程为主线。探究其中预处理、编译、汇编、加载执行、异常与信号、存储、虚拟内存、linux I/O等各个阶段与内容。同时也是对Computer Systems: A Programmer's Perspective (3rd Edition)一书较为完善的总结与梳理。
关键词:hello程序;CSAPP;预处理;编译;汇编;异常;虚拟内存;进程
目 录
第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 -
结论................................................... - 14 -
附件................................................... - 15 -
参考文献................................................ - 16 -
第1章 概述
1.1 Hello简介
P2P:From Program to Process,即从程序到进程的过程。首先程序员利用文本编辑器或是集成编译环境(IDE)在hello.c中以C语言语法规范写下hello程序内容。然后调用gcc编译器,依此通过cpp(预处理器),ccl(编译器),as(汇编器),ld(链接器),生成可执行目标文件hello。该文件可以通过Shell加载到内存中并运行。在这个过程中,用户在Shell中输入“./hello”,系统(Operation System)进程管理利用fork()函数新建进程,并在新的进程中利用exevce()函数将hello在磁盘上的可执行目标文件的内存映像加载到内存中,这样就完成了从Program程序到Process进程的一个过程。
020: From Zero-0 to Zero-0,shell为hello生成进程,并利用evevce()加载程序,mmap创建虚拟内存空间,再将数据加载到虚拟内存中,内核hello进程分配时间片执行逻辑控制流。该程序执行结束,进程进入僵死状态,Shell回收进程,清空进程的所有系统占用,删除所有相关数据结构,抹去所有痕迹。完成了从zero来到zero去的过程。
1.2 环境与工具
在本论文的编写中,用到了如下的软硬件环境与开发调试工具。
硬件环境:Arm64 Apple M1 3.2GHz;8G RAM ;256G SSD。以及X86-64 Intel i5 12490F 4.2GHz;16G RAM;512G SSD。
软件环境:MacOS 13.1;Windows 11;Ubuntu 22.04。
开发和调试工具:Clion 21.04;GCC;GDB;VS Code 22.04。
1.3 中间结果
在编写该论文中,生成了如下的中间结果文件。
1 | Hello.c | 源程序 |
2 | Hello.i | 源文件经过预处理后的到的文件 |
3 | Hello.s | Hello.i经过编译的到的汇编语言程序 |
4 | Hello.o | Hello.s经过汇编后的可重定位目标文件 |
5 | Hello | Hello.o经过链接后的可执行目标文件 |
6 | ELFofhelloo.txt | Hello.o的反汇编文件 |
7 | ELFofhello.txt | Hello的反汇编文件 |
8 | Hellooutref.txt | Hello.o通过readelf读取的节头表 |
9 | Helloref.txt | Hello通过readelf读取的节头表 |
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
1.4 本章小结
本章节简述了Hello的P2P、020过程,阐述了本文章编写过程中使用到的软硬件环境和开发调试工具,并列出了在编写该论文时所生成的中间结果文件。
第2章 预处理
2.1 预处理的概念与作用
在预处理过程中,预处理器(cpp)根据以字符#开头的语句,修改原始的C程序。并输出到.i文件中(可选),该过程有以下三个行为。
1.将源文件中以”include”格式包含的文件复制到编译的源文件中。
2.用实际值替换用“#define”定义的字符串。
3.根据“#if”后面的条件决定需要编译的代码。
经过预处理后的程序的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。也为下一步编译提供便利和先决条件。
2.2在Ubuntu下预处理的命令
Gcc -E hello.c -o hello.i
图 1在Ubuntu下的预处理
2.3 Hello的预处理结果解析
在生成的hello.i文件中,可知经过预处理后的hello.c转为了文件长度显著增加的hello.i文件,预处理器处理“#include”指令在程序源文件头部插入了#include<stdio.h>、#include<unistd.h>、#include<stdlib.h>等头文件中所包含的代码。
图 2 预处理后的hello.i文件
2.4 本章小结
在本章节中,我们介绍了预处理的概念和作用,并展示了在Ubuntu中预处理的实际操作与其结果。
第3章 编译
3.1 编译的概念与作用
编译是指把高级语言翻译通过词法分析、语法分析、语义检查和中间代码生成、代码优化等过程翻译成目标程序的汇编格式。以C语言为例,经过预处理的.i文件中,仍是与机器无关,符合人类阅读逻辑的高级语言。而编译将机器无关的高级语言翻译为机器相关,且机器可以认识的机器语言或汇编语言(又称第二代机器语言)。
通过这种方式,可以使代码面向机器,使得机器可以执行且较好地发挥机器的性能。
3.2 在Ubuntu下编译的命令
Gcc -S hello.i -o hello.s
图 3 hello.i到hello.s的编译
3.3 Hello的编译结果解析
3.3.1数据
在原始c程序代码中,共有如下的各种数据。我们能看到有i、argc、argv三个局部变量,其中argc与argv作为main函数的参数,在该程序中没有全局变量。以及有printf的格式串,还有这些代码也是作为数据,文章将一一论述,
对于局部变量,编译器先将栈指针减去32,然后再将第一个参数和第二个参数压入栈中,其中第一个参数是一个32位整型值,用来指示argv中参数串个数的值,argv为一个指向一个指针数组的指针,数组中每个指针都指向用户在运行程序时指定的参数。对于i,编译器也是将其存储在rsp-4的位置中。这些局部变量,在程序运行时,执行到相关的语句时,才在栈中分配内存,赋予初始值。
对于格式串,它们被存在.rodata只读节中,作为printf的输出部分。
对于代码,他们都在.text只读且可执行节中,可读可执行,不可修改。
3.3.2赋值
在汇编中,赋值是通过mov指令实现的。编译器将参数寄存器中的值移动到栈中,栈中的临时变量即被赋予了初始值。而对于局部变量来说,他们也是在栈中初始化与赋予初始值的,编译器会为他们在栈中开辟预留空间,当执行到与这些局部变量赋值等相关的代码时,mov指令就会在他们在栈中的位置赋予程序员所设置好的初始值。
3.3.3关系操作
在if语句的判断中。需要用到关系操作如!=(不等于)、<=(小于等于)等关系操作来判断条件是否成立。在汇编中用cmp指令来实现关系操作。其是将两个作比较的数作差,在这个过程中会设置标识位CC,再通过标识位来判断两个数的关系情况。If及for语句的实现,在汇编语言中通常是cmp与条
件jump指令结合起来实现。
3.3.4数组/指针/结构操作
在该程序中,有声明为char *argv[]的指针数组。Argv即为指向该指针数组首地址的指针。Main函数开始时,编译器将放置在rsi中argv的值压入栈中,再通过访问栈来获取argv的值,本程序在调用printf函数时需要调用argv[1]&argv[2],所指向的用户输入的字符串。所以在汇编语言中,先定位到argv指向的地址,再读取指针数组中1&2号元素的值,再将此地址指向的串的地址赋予rdi&rsi寄存器,然后调用printf函数。
图 6指针数组操作
3.3.5转移控制
该程序使用了if以及for的转移控制。其中if通过cmp和jump指令来共同实现,cmp用来比较条件,设置标识位,jump条件跳转通过判断标志位寄存器的值来选择是否进行跳转。For语句的实现也是cmp和jump指令来配合实现,通常在汇编语言程序中翻译为while和dowhile格式,初始化判断条件,判断条件,是否再次进入循环体。
3.3.6函数操作
首先分析主函数main,主函数有两个参数分别为argc、argv其中Argv为指向指针数组首地址的指针,指针数组内每一个指针均指向用户输入的一个参数。而argc为一个32位整形变量,用来指示argv数组中参数(元素)的个数。按照规则,第一个参数用rdi寄存器传递,第二个参数用rsi寄存器传递,第三个rdx寄存器传递。
函数调用使用了call指令,在main函数中,调用了printf,exit,sleep,atoi,getchar等函数。call指令会将下一条指令的地址压入栈中,并会将PC寄存器的值修改为他们所调用的函数的首地址。
图 7 call指令的函数操作
局部变量会在栈帧中保存,当函数返回时会将保存在栈帧中的变量还原。
图 8 hello.s
3.4 本章小结
本章节介绍了编译的概念及作用,并详细分析了经过编译后的汇编语言代码。指出了C语言的数据与操作在汇编语言中的实现。
第4章 汇编
4.1 汇编的概念与作用
汇编是指汇编器将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中。
这样做可以将汇编代码转化成机器可以理解并执行的文件(可重定位目标文件)。
4.2 在Ubuntu下汇编的命令
Gcc -c hello.s -o hello.o
图 9 汇编与反编译
4.3 可重定位目标elf格式
4.3.1ELF头
ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统 的字的大小和字节顺序,ELF 头剩下的部分包含帮助链接器语法分析和解 释目标文件的信息,其中包括 ELF 头的大小、目标文件的类型、机器类型、 字节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量等信息。
图 10 ELF头表 与节头部表
4.3.2节头部表
在节头部表中可以看到各个节的类型,大小,地址,偏移量等信息。
4.3.3重定位节
汇编器把对所有不确定最终位置的对外部数据或外部函数的引用放进重定位条目里。在链接时,链接器循着这些条目信息更改条目位置数据的引用位置。
图 11重定位节
其中R_X86_64_PLT32& R_X86_64_PC32为两种最常见的寻址方式
4.3.4符号节
符号表存放在程序中定义和引用的函数和全局变量以及静态局部变量的信息。其中,name是字符串表中的字节偏移,指向符号的以null结尾的字符串名字;value是符号的地址。对于可定位目标文件来说,value是距定义目标的节的起始位置的偏移。对于可执行目标文件来说,该值是一个绝对运行的地址。size是目标的大小(以字节为单位),type通常要么是数据要么是函数。bind字段表明符号是本地的还是全局的。
图 12符号节
4.4 Hello.o的结果解析
图 13反编译hello
4.4.1机器语言的构成:
机器语言是用二进制代码表示的计算机能直接识别和执行的一种机器指令的集合。一条指令就是机器语言的一个语句,它是一组有意义的二进制代码,指令的基本格式如,操作码字段和地址码字段,其中操作码指明了指令的操作性质及功能,地址码则给出了操作数或操作数的地址。
4.4.2与汇编语言的映射关系:
汇编指令是机器指令便于记忆的书写格式。每行的操作码都与其后的汇编指令一一对应。
4.4.3操作数:
反汇编代码中的立即数是十六进制数,而 hello.s 文件中的数是十进制的。寄存器寻址两者相同。内存引用hello.s 中会用伪指令(如.LC0)代替,而反汇编则是基址加偏移量寻址:0x0(%rip)。
4.4.4条件分支:
在hello.s文件中,所有的跳转指令后都会接.Lx一类的段名称,但在反汇编操作得到的汇编代码中,跳转指令后只会接对应的地址。
4.4.5函数调用:
在hello.s中,用call指令进行调用函数时,总会在call指令后直接加上函数名,而在反汇编得到的汇编代码中,call指令后会跟着的是下一条指令的地址(一般以main地址加偏移量的形式给出)。
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
4.5 本章小结
本章节探究了从汇编代码到机器代码的转化过程,并分析了ELF可重定位文件中ELF头以及各个节的作用,还将机器代码与汇编代码的一些显著差异作了分析。
第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
图 14 ld链接命令
5.3 可执行目标文件hello的格式
5.3.1ELF头
在ELF头表中列出了文件的基本信息,包括程序的入口地址。
图 15ELF头表
5.3.2节头表
图 16 节头表
5.3.3程序头
图 17 程序头
程序头中列出了加载时的顺序。
5.3.4符号表
符号表中保存着定位、重地位程序中符号定义和引用的信息
图 18 符号表
5.4 hello的虚拟地址空间
图为在edb的symbolview中查看各段地址信息
图 19 各段/部分 虚拟地址信息
在图中可以看到各段的起始地址与5.3中看到的基本一致,并且增加了一些外部函数的地址和一些其他信息。
5.5 链接的重定位过程分析
命令objdump -d -r hello > ELFofHello
经过链接后的可执行目标文件的内容与可重定位目标文件有明显的不同,可执行目标文件有.init,.plt,.finl。其中.init节包含进程初始化时要执行的程序指令,.plt节包含函数连接表,.fiinl节包含进程终止时要执行的程序指令。
在连接的过程中,具体地执行以下两步骤。
1.空间与地址分配,扫描所有的输入目标文件,获得它们的各个段的长度、属性和位置,并且将这些输入目标文件中的符号表中所有的符号定义和符号引用收集起来,统一放到一个全局符号表。再将它们合并。
2.符号解析与重定位。使用上一步收集到的所有信息,读取输入目标文件中段的数据、重定位信息,并且进行符号解析与重定位、调整代码中的地址等。对于重定位的具体执行的伪代码如下。
图 20 重定位算法
对于hello在执行重定位,以图21重定位条目表第1行内容来举例说明。该行有一个重定位条目:类型为R_X86_64_PC32,offset为0x1c,addend为-4,symbol为.rodata;ADDR(main)和ADDR(.rodata)均可在hello的重定位表或者hello执行文件的反汇编文件中得到,再利用图20中6~8行算法即可得到更新的内容(一个相对地址)。
图 21 重定位条目例
5.6 hello的执行流程
子程序名 | 程序地址 |
ld -2.33.so!_dl_start | 0x7f641a388df0 |
ld-2.33.so!_dl_init | 0x7f641a398c10 |
hello!_start | 0x401125 |
libc-2.33so!__libc_start_main | 0x7f6fe58bd550 |
hello!printf@plt | 0x401040 |
hello!atoi@plt | 0x401060 |
hello!sleep@plt | 0x401080 |
hello!getchar@plt | 0x401050 |
libc-2.33.so!exit | 0x7f6fe58b40d0/0x401070 |
5.7 Hello的动态链接分析
在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。GNU编译系统使用延迟绑定(lazybinding),将过程地址的绑定推迟到第一次调用该过程时。
延迟绑定是通过GOT和PLT实现的。GOT是数据段的一部分,而PLT是代码段的一部分。两表内容分别为:
PLT:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。
GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。
根据hello ELF文件可知,GOT起始表位置为0x404000,如图:
图 22 GOT起始表地址
edb执行init之前的内容
图 23 edb执行init之前的内容
edb执行init之后的内容
调用_start之后发生改变,0x404008后的两个8个字节分别变为:0x7fb9561592e0、0x7fb956133d30其中GOT[O](对应0x403e50)和GOT[1](对应0x7fb9561592e0)包含动态链接器在解析函数地址时会使用的信息。包含动态链接器在解析函数地址时会使用的信息。GOT[2](对应0x7fb956133d30)是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数
5.8 本章小结
本章介绍了链接的概念及作用,对hello的可执行目标文件ELF格式进行了详细的分析,介绍了hello的虚拟地址,分析了hello的重定位过程、执行流程及其子程序地址、动态链接过程,详细阐述了hello.o链接成为一个可执行目标文件的过程。
第6章 hello进程管理
6.1 进程的概念与作用
进程的经典定义就是一个执行中程序的实例。进程还是计算机中最伟大的抽象之一,是对CPU、主存、I/0的抽象。
在现代系统上运行一个程序时,我们会得到一个假象,就好像我们的程序是系统中当前运行的唯一的程序一样。我们的程序好像是独占地使用处理器和内存。处理器就好像是无间断地一条接一条地执行我们程序中的指令。最后,我们程序中的代码和数据好像是系统内存中唯一的对象。这些都是进程带给我们的作用。
6.2 简述壳Shell-bash的作用与处理流程
Shell的处理流程:
- 解析输入的命令行参数;
- 该命令是内置的 shell 命令则马上解释该命令;
- 若该命令是一个可执行目标文件,会在一个新的子进程的上下文中 execve并运行这个文件。
- 运行过程中,shell 还会处理异常,如从键盘输入 Ctrl+C 终止子进程,或是Ctrl+Z将前台运行的程序挂起;
- 进程终止后,shell 负责回收该子进程。
6.3 Hello的fork进程创建过程
新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大的区别在于它们有不同的PID。
6.4 Hello的execve过程
当创建了一个子进程之后,子进程调用exceve函数在当前子进程的上下文加载并运行一个新的程序即hello程序,加载并运行共有以下四个步骤:
1.删除已存在的用户区域。删除当前进程虚拟地址的用户部分中已存在的区域结构。
2.映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些区域结构都是私有的,写时复制的。虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区。bss区域是请求二进制零的,映射匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零。
3.映射共享区域。如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域。
4.设置程序计数器(PC/RIP寄存器)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。下一次调用这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
6.5 Hello的进程执行
6.5.1进程上下文
上下文就是内核重新启动一个被抢占的进程所需要恢复的原来的状态,由寄存器、程序计数器、用户栈、内核栈和内核数据结构等对象的值构成。初始时,控制流再hello内,处于用户模式。调用系统函数sleep后,进入内核态,此时间片停止。Ts(由用户输入的第三个参数的值)后,发送中断信号,转回用户模式,继续执行指令。
6.5.2进程时间片与逻辑控制流
当进程被创建时,OS会给每个正在运行的进程分配一段CPU时间,当这段时间结束后,系统资源会被其他进程抢占。而一系列程序计数器的值构成的序列叫做逻辑控制流,当进程被抢占时,逻辑控制流会轮到其他进程。
6.5.3进程调度
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行,我们说内核调度了这个进程。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。
6.5.4用户态与核心态
处理器使用一个寄存器提供两种模式的区分。用户态的进程不允许执行特殊指令,不允许直接引用地址空间中内核区的代码和数据;内核态进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
核心态拥有最高的访问权限,处理器以一个寄存器当做模式位来描述当前进程的特权。进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他情况下始终处于用户权限之中,保证了系统的安全性。
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
6.6 hello的异常与信号处理
在hello执行的过程中,可能会出现下列四种异常:中断、陷阱、故障、终止。
类别 | 原因 | 异步/同步 | 返回行为 |
中断 | 来自I/O设备的信号 | 异步 | 总是返回到下一条指令 |
陷阱 | 有意的异常 | 同步 | 总是返回到下一条指令 |
故障 | 可能可恢复的错误 | 同步 | 可能返回到当前指令 |
终止 | 不可恢复的错误 | 同步 | 不会返回 |
在hello执行中。
中途乱按,(若hello处于sleep中)键盘的输入会被存入缓冲区,被认为是命令。
若从键盘按下Ctrl+Z:进程收到 SIGSTP 信号, hello 进程挂起。用ps查看其进程PID,可以发现hello的PID是7167;再用jobs查看此时hello的后台 job号是1,调用 fg 1将其调回前台。
若从键盘Ctrl+C:进程收到 SIGINT 信号,结束 hello。在ps中查询不到其PID,在job中也没有显示,hello已经被彻底结束。
若使用Kill命令:挂起的进程被终止,在ps中无法查到到其PID,jobs中也无此任务。
正在上传…重新上传取消
6.7本章小结
本章节阐述了进程的概念与作用,Shell的作用与一般流程,分析了hello从Shell中通过fork、execve创建、加载、终止的过程,还分析了hello的异常与信号处理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
7.1.1逻辑地址
指由程序产生的段内偏移地址。有时候直接把 逻辑地址当做虚拟地址。逻辑地址与虚拟地址二者之间没有明确的界限。
7.1.2虚拟地址
指由程序产生的由段选择符和段内偏移地址组成的地址。这2部分组成的地址并不能直接访问物理内存,而是要通过分段地址的变化处理后才会对应到相应的物理内存地址。
线性地址是虚拟地址到物理地址变换的中间层,是处理器可寻址的内存空间中的地址。程序代码会产生逻辑地址,也就是段中的偏移地址,加上相应的段基址就成了线性地址(段基址+逻辑地址)。如果开启了分页机制,那么线性地址需要再经过变换,转为为物理地址。如果无分页机制,那么线性地址就是物理地址 。
物理地址是指内存中物理单元的集合,是地址转换的最终地址虚拟地址到物理地址的转换是与体系结构相关的,一般有分段和分页两种方式。
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
7.2 Intel逻辑地址到线性地址的变换-段式管理
给定一个逻辑地址。将逻辑地址进行划分得到索引、TL、RPL信息。选择是GDT还是LDT中的段,再根据相应的寄存器得到地址。寻找段描述符得到基地址。线性地址=基地址+偏移量。
实模式下,逻辑地址=线性地址=实际的物理地址。段寄存器存放真实段基址,给出32位地址偏移量,则可以访问真实物理内存。
7.3 Hello的线性地址到物理地址的变换-页式管理
页式管理是一种内存空间存储管理的技术,页式管理分为静态页式管理和动态页式管理。将各进程的虚拟空间划分成若干个长度相等的页(page),页式管理把内存空间按页的大小划分成片或者页面(page frame),然后把页式虚拟地址与内存地址建立一一对应页表,并用相应的硬件地址变换机构,来解决离散地址变换问题。页式管理采用请求调页或预调页技术实现了内外存存储器的统一管理。
7.4 TLB与四级页表支持下的VA到PA的变换
当TLB与四级页表相结合其地址翻译过程如下:先将这个虚拟地址的VPN分为TLB标记部分和TLB索引部分检查是否再TLB命中如果命中直接取出物理地址,否则的化虚拟地址被划分为4个VPN和一个VPO每个VPN(i)对应了第i级页表的索引,通过这个索引最后对应了一个固定的PPN将这个PPN与VPO结合得到新的物理地址,并把这个物理地址的信息存入TLB缓存。
7.5 三级Cache支持下的物理内存访问
图 28 Intel i7的Cache实现
7.6 hello进程fork时的内存映射
7.7 hello进程execve时的内存映射
当创建了一个子进程之后,子进程调用exceve函数在当前子进程的上下文加载并运行一个新的程序即hello程序,加载并运行共有以下四个步骤:
1.删除已存在的用户区域。删除当前进程虚拟地址的用户部分中已存在的区域结构。
2.映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些区域结构都是私有的,写时复制的。虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区。bss区域是请求二进制零的,映射匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零。
3.映射共享区域。如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域。
4.设置程序计数器(PC/RIP寄存器)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。下一次调用这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
7.8 缺页故障与缺页中断处理
DRAM缓存的不命中称为缺页。DRAM缓存的不命中触发一个缺页故障,缺页故障用内核中的缺页异常处理程序,该程序会选择一个牺牲页,如果该牺牲页已经做了更改,那么内核会将它复制回磁盘,否则不会进行复制即写回,然后将牺牲页从DRAM中出去,更新该页的位置放入待取的页面。然后CPU重新执行造成缺页故障的命令此时将可以正常运行。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显示地保留为供应用程序使用。空闲块用来分配,空闲块保持空闲,直到它显示地被应用所分配。一个已分配地块保持已分配状态直到它被释放,这种释放要么是应用程序显式执行地,要么是内存分配器自身隐式执行地。
7.10本章小结
本章主要介绍了hello存储器的地址空间;虚拟地址到物理地址的转换;cache的物理内存访问;进程 fork、execve 时的内存映射、缺页故障与缺页中断处理;动态存储分配管理。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
普通文件:包含人一数据。应用程序常常需要区分文本文件和二进制文件,文本文件是只含有ASCLL或Unicode字符的普通文件;二进制文件是所有其他文件,对于内核来说这二者没有却别。
目录:是包含一组链接的文件,其中每一个链接都将一个文件名映射到另一个文件。
其他文件类型包括命名通道、符号链接以及字符和块设备。
设备管理:unix io接口,包括打开和关闭文件,读和写文件以及改变当前文件的位置。
8.2 简述Unix IO接口及其函数
打开和关闭文件:Open()函数:这个函数回打开一个已经存在的文件或者创建一个新的文件,可以添加参数只读,只写和可读可写。Close()函数:这个函数关闭一个已经打开的文件。
读和写文件:应用程序通过分别调用read和write函数来执行输入和输出的。其中两个函数在unisted.h中定义:ssize_t read(int fd, void *buf, size_t n);Ssize_t write(int fd, const void *buf, size_t n);
read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf返回值-1表示一个错误,返回值为0表示EOF。否则返回值表示的是实际传送的字节数量。
write函数从内存buf位置复制至多n个字节到描述符fd的当前文件位置。改变文件位置。
8.3 printf的实现分析
我们从源码中开始分析
int printf(const char *fmt, ...)
{
int i;
char buf[256];
va_list arg = (va_list)((char *)(&fmt) + 4);
i = vsprintf(buf, fmt, arg);
write(buf, i);
return i;
}
//printf接受了一个fmt的格式,然后将匹配到的参数以fmt格式输出。printf函数中调用了两个系统vsprintf与write,其中vsprintf函数:
int vsprintf(char *buf, const char *fmt, va_list args)
{
char *p;
char tmp[256];
va_list p_next_arg = args;
for (p = buf; *fmt; fmt++)
{
if (*fmt != '%')
{
*p++ = *fmt;
continue;
}
fmt++;
switch (*fmt)
{
case 'x':
itoa(tmp, *((int *)p_next_arg));
strcpy(p, tmp);
p_next_arg += 4;
p += strlen(tmp);
break;
case 's':
break;
default:
break;
}
}
return (p - buf);
}
//vsprintf的作用是将所有参数内容格式化后存入buf,然后返回格式化数组的长度。
//而另一个函数write在上一小节中有介绍。
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。实现如下:
int getchar(void){
static char buf[BUFSIZ];
static char *bb = buf;
static int n = 0;
if(n == 0){
n = read(0, buf, BUFSIZ);
bb = buf;
}
return(--n >= 0)?(unsigned char) *bb++ : EOF;
}
8.5本章小结
本节介绍了linux的IO管理,以及其下相关函数。还分析了printf函数与getchar函数的实现。
结论
Hello的一生中所经历的过程如下。
- 预处理:编译器对源程序进行预处理,修改C程序,将C程序以#开头的头文件引用的代码和宏定义插入源程序,得到hello.i文件。
- 编译:编译器将高级语言hello.i翻译为机器可以识别的汇编语言hello.s。
- 汇编:汇编器将hello.s翻译为机器语言指令,并生成可重定位目标文件,将结果输出到hello.o中。
- 连接:链接器将hello.o中重定位条目中所需的外部函数,外部变量所在的外部库与hello.o连接起来,的到可执行目标文件。
- 加载运行:在shell中输入“.hello 学号 姓名 显示时间间隔”,shell调用fork()函数创建子进程,再在子进程中调用execve()函数,加载hello程序。加载器将映射虚拟内存,静如程序入口后,程序载入物理内存进入main函数中开始执行。
- 运行时:在hello程序运行时,会调用一些与linux I/O设备管理相关的库函数如printf。
- 回收:hello在运行结束后会变为僵死进程,最后会被shell回收,内核会删除该进程的所有信息与系统占用。
感悟如下。
在这个学期中,经过老师们认真负责的讲解,辅以对CSAPP深入理解计算机系统这本教材的解读,和四个实验的实践。我们对现代计算机系统构成有了一定水平的理解。这门课程作为大二上学期开设的一门极其重要的专业基础课程对我们计算机思维及其体系的构建起到了极大的作用。尝试深入理解了程序在操作系统上从编写到运行再到回收的整个过程,在其中对信息的表示,程序的机器级表示,计算机处理器体系结构,程序的优化,存储器体系结构,连接,信号等都有了一定了了解和应用能力。这是一个从工科通识学生转向计算机科学与技术专业学生的重要过程。
对计算机系统的设计与实现的理解,对后续专业课程的深入学习和日后科研与工作的开展奠定了重要的基础。同时也在这门课程中感受到,要成为一名真正的计算机科学与技术领域的成员,只会写代码是远远不够的,而是要具备对计算机系统的底层认知,具备优化程序,跨学科教材,完成工程任务等更全面的能力。
附件
1 | Hello.c | 源程序 |
2 | Hello.i | 源文件经过预处理后的到的文件 |
3 | Hello.s | Hello.i经过编译的到的汇编语言程序 |
4 | Hello.o | Hello.s经过汇编后的可重定位目标文件 |
5 | Hello | Hello.o经过链接后的可执行目标文件 |
6 | ELFofhelloo.txt | Hello.o的反汇编文件 |
7 | ELFofhello.txt | Hello的反汇编文件 |
8 | Hellooutref.txt | Hello.o通过readelf读取的节头表 |
9 | Helloref.txt | Hello通过readelf读取的节头表 |
参考文献
[1] Unix I/O介绍 https://www.jianshu.com/p/5228e027bbcb
[2] 百度百科 时间片https://baike.baidu.com/item/时间片/6525414?fr=aladdin .
[3] 百度百科 虚拟内存 https://baike.baidu.com/item/虚拟内存/101812?fr=aladdin
[4] Randal E.Bryant/ David O’Hallaron Computer Systems: A Programmer's Perspective (3rd Edition)