ICS-程序人生

摘要

hello是每一位程序员编写的第一个程序,本文通过对这个简单程序的分析,从最开始的.c文件开始, 预处理生成.i文件,再经过编译生成的.s文件,汇编得到的二进制可重定位目标文件,链接生成一个可执行程序。hello就可以开始执行了,执行的过程中,hello需要进程管理,存储管理,I/0管理来调度。本文分章节介绍了每一个过程的操作,更好地说明了计算机的底层实现,希望可以使读者通过hello的一生能够更加深入的理解计算机系统的工作原理。
关键词:计算机系统;程序的生命周期;进程管理;存储管理

目录

第1章 概述

1.1 Hello简介

hello程序的生命周期是从一个高级C语言程序开始的。GCC编译器驱动程序读取源程序文件hello.c,并把它翻译成一个可执行目标文件hello 这个翻译过程可分为四个阶段完成,如图所示。
图 1 编译系统的流程
预处理阶段,编译阶段,汇编阶段,结果就得到hello文件,它是一个可执行目标文件,可以被加载到内存中,由系统执行。
在shell中通过一系列指令的调用将输入的字符读入到寄存器中,之后将Hello目标文件中的代码和数据从磁盘复制到主存。调用fork产生一个子进程,然后hello便从程序变成了一个进程(:program to progress,P2P)。
在shell中开始执行并为其映射出虚拟内存,然后在开始运行进程的时候分配并载入物理内存,开始执行hello的程序,将output显示到屏幕,最后hello进程结束,shell回收内存空间(zero to zero,020)。

1.2 环境与工具

硬件环境:AMD Ryzen 5 4600H with Radeon Graphics 3.00 GHz;
16.0 GB (15.4 GB 可用) RAM;
软件环境:Windows10 64位;VirtualBox 6.1;Ubuntu 20.04
开发工具:Visual Studio 2017 64位;CodeBlocks 64位;vim/gedit+gcc(9.3.0)

1.3 中间结果

列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。

hello.c源代码
hello.i预处理后的文本文件
hello.s汇编后文本文件
hello链接之后可执行文件
hello.out反汇编之后的可重定位文件
hello.elfhello的elf文件

1.4 本章小结

本章主要介绍了hello的P2P,020过程,以及进行实验时的软硬件环境及开发与调试工具和在本论文中生成的中间结果文件。

第2章 预处理

2.1 预处理的概念与作用

**概念:**预处理过程是编译过程的第一步,预处理器(cpp)根据以字符#开头的命令(#include头文件和#define宏定义等),修改原始的C程序,将引用的所有库展开合并成为一个完整的文本文件,得到了另一个C程序,通常 是以.i作为文件扩展名。
**作用:**根据条件编译指令等编译命令及其后的条件,将源程序中的某部分包含进来或排除在外。令将头文件中的定义统统都加入到它所产生的输出文件中。识别一些特殊的符号。

2.2在Ubuntu下预处理的命令

图 2 在Ubuntu下预处理命令
图 3 指令结果

2.3 Hello的预处理结果解析

图 4 源程序hello.c
图 5 预处理后的部分hello.i文件

  1. 代码行数从23行增加到3060行。
  2. 预处理的结果插入所有用#include命令指定的文件,并扩展所有用#define声明指定的宏。
  3. main函数在hello.i的最后。
    图 6 部分头文件路径
    图 7 宏替换部分示例

2.4 本章小结

本章主要介绍了预处理的概念和应用功能,以及Ubuntu下预处理的两个指令,通对比hello.c文件和预处理结果hello.i文本文件解析,详细了解了预处理的细节,过程和结果。

第3章 编译

3.1 编译的概念与作用

**概念:**编译将把用C语言提供的相对比较抽象表示的程序转化成处理器执行的非常基本的指令,它包含一个汇编语言程序。他是从 .i 到 .s 即预处理后的文件到生成汇编语言程序。
**作用:**编译得到的汇编代码表示非常接近于机器代码。与机器代码的二进制格式相比,汇编代码的主要特点是它用可读性更好的文本格式表示。可见一些通常对C语言程序员隐藏的处理器状态。例如程序计数器,整数寄存器,条件码寄存器。

3.2 在Ubuntu下编译的命令

图 8 Ubuntu下的编译命令图 9 编译的结果

3.3 Hello的编译结果解析

图 10 hello.s开头
**指示(Directives):**以‘. ’开头的行都是指导汇编器和链接器工作的伪指令。我们通常可以忽略这些行。
**标签(Labels);**以‘ :’结尾,用来把标签名和标签出现的位置关联起来。例如,标签.LC0:表示紧接着的字符串的名称是 .LC0。按照惯例, 以点号开始的标签都是编译器生成的临时局部标签,其它标签则是用户可见的函数和全局变量名称。
指令(Instructions): 实际的汇编代码 (pushq %rbp), 一般都会缩进,以便和指示及标签区分开来。

3.3.1 数据操作

C语言的数据类型有常量,变量(全局/局部/静态),表达式,类型,宏。

1.常量(字符串)

图 11 字符串示例
c语言中代码为printf(“用法: Hello 学号 姓名 秒数!\n”)编译为为"\347\224\250\346\263\225:Hello\345\255\246\345\217\267\345\247\223\345\220\215 \347\247\222\346\225\260\357\274\201",
printf传入的格式化参数。在hello.s,注意到字符串使用UTF-8的格式编码的,一个汉字在UTF-8中占三个字节。
“Hello %s %s\n”,仍然是由printf函数传入的格式化参数,hello.s声明如下。
两个字符串都被存放在.rodata(只读数据段)。

2.局部变量

图 12 局部变量
局部变量i被储存在-4(%rbp)中,占4个字节,属于int型。

3.数组

图 13 数组
数组char argv[],指针类型均为8个字节大小。一般从栈底指针开始分配。

3.3.2 赋值

如上图11,直接对i赋值,movl指令。Move指令加后缀可以表示字节数。

3.3.3 算术操作

  1. 加法操作add:在对计数器加一时addq,“i+1”的
  2. 减法操作sub:为main函数开辟栈帧是将栈顶指针-0x32。
  3. 地址的运算:-常数(地址),整个字符串首地址—+相对地址。
  4. 加载有效地址:将LC0的有效地址传送给%rdi。
    图 14 算术运算示例

3.3.4关系操作

  1. 对于if(argv != 4),汇编语言中首先设置条件码,然后根据条件码来进行控制转移。
    图 15 if句
  2. 对于for(i=0;i<8;i++),在hello.c作为判断循环条件,在汇编代码被编译为:cmpl
    图 16 for语句

3.3.5 控制转移

如图14,15,条件控制然后跳转,jump语句.

3.3.6 函数操作

函数调用涉及到参数传递(地址/值)、函数调用()、局部变量、函数返回

  1. 参数传递:调用函数前,先把需要的参数传入相应的寄存器中。X86-64中,过程调用传递参数规则:第1~6个参数一次储存在%rdi、%rsi、%rdx、%rcx、%r8、%r9这六个寄存器中,剩下的参数保存在栈当中。
  2. 函数调用:程序计数器设置为被调用函数的起始地址,在接下来返回中,程序计数器再设置为调用指令的下一条指令的地址。通过系统的内部指令调用语句call对main函数进行函数调用。
  3. 函数返回:函数执行完进行return操作。
    图 17 return

3.4 本章小结

本章简要的介绍了编译的概念和作用,实质上就是将hello.c的c语言代码转换成汇编代码对应的语句的一个翻译过程,汇编代码是后续变成机器代码的基础。同时简要说明了编译器如何处理各种各样的数据和操作以及汇编代码的体现。

第4章 汇编

4.1 汇编的概念与作用

**概念:**驱动程序运行汇编器as,将汇编语言(hello.s)翻译成机器语言(hello.o)的过程称为汇编。
**作用:**将高级语言转化为机器可直接识别执行的代码文件:将.s汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序格式并保存在.o二进制文件中。

4.2 在Ubuntu下汇编的命令

图 18 在Ubuntu下的汇编命令
图 19 命令结果

4.3 可重定位目标elf格式

使用 readelf -a hello.o > helloo.elf 指令获得 hello.o 文件的 ELF 格式。
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。

  1. ELF头:在elf文件最上方。以一个16字节序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序,剩下的信息帮助链接器语法分析和解释目标文件的信息。包括ELF头大小,目标文件类型,机器类型,节头部表的文件偏移以及节头部表中条目的大小和数量。
    图 20 ELF头
  2. 节头:描述了不同节的类型,位置以及大小。
    图 21 节头
  3. Key to Flags:标志的解释信息。
    图 22 Key to Flags
  4. 没有程序头
  5. 重定位节:表述了各个段引用的外部符号等。在链接时,需要通过重定位节对这些位置的地址进行修改。链接器会通过重定位条目的类型通过偏移量等信息计算出正确的地址。
    可以看出,八个项目分别为,第一个printf中的字符串数据,puts函数,exit函数,第二个printf中的字符串数据,printf函数,sleep函数,getchar函数。
    ···
    注:重定位条目中信息(Info)包括symbol和type两部分,symbol占前4字节,type占后4字节。Symbol代表重定位到的目标在.symtab中的偏移量,type代表重定位类型。
    ···
    offset是需要进行重定位到的目标的偏移位置。在计算条目重定位地址时,首先计算该条目在.text中的地址,通常采用基址+相对地址的方式,再计算运行时的地址,将其与下一条指令的地址之差作为绝对地址,也就是该条目重定位之后的地址。
    r.offset偏移量, r.symbol符号名称, r.type=类型,r.addend加数,重定位使用PC相对寻址方式运算方法如下:
    图 23 PC相对寻址方式运算方法图 24 重定位节
  6. 最后两个部分分别是eh_frame节的重定位信息和用来存放程序中定义和引用的函数和全局变量的信息的符号表。
    图 25 重定位节

4.4 Hello.o的结果解析

反汇编代码中有机器指令以及每一个函数之间的相对地址。
**数据操作:**汇编文件里直接展示了数据的内容,但是在反汇编文件里,看不到数据中的具体信息。
分支转移:在汇编代码中,分支跳转是直接以.L0等助记符表示,跳转到条目,指令后面是条目名称。但是在反汇编代码中,分支转移表示为主函数+段内偏移量,反汇编里的跳转指令后面是已经计算好的地址。
**函数调用:**汇编代码中,函数调用时call后直接是函数名@PLT,反汇编中call之后是main+段内偏移量。注意此时的段偏移量全部定位到call的下一条指令,是假的偏移,因为hello.o中所调用的函数全都是都共享库中的函数,需要经过动态链接器链接之后才能最终确定运行时的执行地址。尚未链接时,call后的相对地址全置为0,并且添加重定位的信息,等待链接。在反汇编中,函数调用指令跟着被跳转函数的相对地址,如果被调用函数是程序员自己定义的函数,这使得相对地址就是所调用函数的地址。
**全局变量访问:**汇编代码,全局变量访问.LC0(%rip)。反汇编中,全局变量访问0x0(%rip)(此时尚未进行重定位,全局变量全都初始化为0+%rip)。
在这里插入图片描述
在这里插入图片描述
图 26 hello.c反汇编结果

4.5 本章小结

本章讲述了从hello.s到hello.o的过程,这中间需要经过汇编器(as)的帮忙,生成的文件是用机器语言写的二进制文件,我们无法直接查看.o文件,但是可以使用 readelf 指令获得 hello.o 文件的 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

图 27 在Ubuntu下链接的命令
图 28 命令结果

5.3 可执行目标文件hello的格式

. ELF头:与图18相比,文件类型,程序头大小,程序入口地址改变,节头的数量也增加了。同时,图18的.o文件中,地址位都是零,而在可执行文件hello中,地址数据已经存在了。说明可重定位文件没有定位,可执行文件中,代码都已经定位到了最终执行的地址,有关定位的rel.text节就没有了。同时出现程序表。

  1. 入口点地址:0x4010f0
    图 29 ELF头
  2. 节头:在节头中对hello中所有的节信息进行了声明,给出各节size大小,地址Address和在程序中的偏移量offset,已经链接好的程序,就可以根据标出的信息确定程序实际被加载到虚拟地址的地址。
    在这里插入图片描述
    图 30 节头
    1)Key to flags部分等无差别。
    图 31 key to flags
    2)程序头:描述了各节的分布和数值范围,类似一个目录。
    图 32 程序头
    3)段节
    图 33 段节
    4)Dynamic section:如果程序进行了动态链接,就会出现动态节。
    图 34 动态节
    5)重定位节:
    图 35 重定位节

5.4 hello的虚拟地址空间

  1. edb的Data Dump窗口可以看出虚拟地址是从0x401000开始,0x402000结束。
    与hello的节头对应
    图 36 data dump
    图 37 程序头
  2. edb的Symbols窗口可以看出和节头中的程序的虚拟地址一一对应。
    图 38 edb中的Symbols

5.5 链接的重定位过程分析

运用指令objdump -d -r hello 得到hello的反汇编文件。
图 39 hello反汇编的结果

  1. hello.o中只有一个.text节,而且只有一个main函数,函数地址也是默认的0x000000。Hello反汇编结果多了很多文件节,如.init,.plt,节,而且每个节中有许多函数。
    图 40 .plt
    2.hello.o中地址都是相对偏移地址,hello中使用的是函数的虚拟地址。
    图 41 可以看到虚拟地址
    3.hello.o中的重定位条目在链接后不再出现在hello中。hello的反汇编中增加了许多外部链接的共享库函数。如puts@plt,printf@plt,getchar@plt函数等。
    图 42 观察共享函数库
    4.重定位是重新计算各个目标的地址过程。hello的重定位是合并相同的节,确定新节中所有定义符号在虚拟地址空间中的地址,还要对引用符号进行重定位,这用到在.rel_data和.rel_text节中保存的重定位信息。

5.6 hello的执行流程

图 43 edb搜索示例

名称地址
_init0x401000
.plt0x401020
puts@plt0x401030
exit@plt0x401060
printf_chk@plt0x4010b0
sleep@plt0x401070
getc@plt0x4010e0
_start0x4010f0
main0x4011d6
_libc_csu_init0x401260
_libc_csu_fini0x4012d0
_fini0x4012d8

5.7 Hello的动态链接分析

动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而非静态链接一样把所有程序模块都链接成一个单独的可执行文件。
当共享库定义的函数被调用时,编译器无法得到函数运行时的具体地址,因为定义它的共享模块在运行时可以加载到任何位置。于是存在延迟绑定的概念,将过程地址的绑定推迟到第一次调用该过程时。GOT和过程链接表PLT的协作来解析函数的地址。在加载时,动态链接器会重定位GOT中的条目,使它包含正确的绝对地址,而PLT中的每个函数负责调用不同函数。
图 44 dl_init运行前
图 45 dl_init运行前
0x40400后8个字节和0x40410前八个字节的变化包含了动态链接器在解析函数地址时会使用的信息。

5.8 本章小结

本章介绍了链接的概念及作用以及命令,静态链接将目标文件和库文件打包至一个可执行的文件中,而动态链接把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起,同时我们对hello的elf进行了分析,看了hello的虚拟空间,了解重定位过程,对链接有了更深的认识。

第6章 hello进程管理

6.1 进程的概念与作用

**概念:**一个执行程序中的实例,系统中的每个程序都运行在某个进程的上下文中,进程是程序的一次执行过程,包含程序的代码数据段,还包括堆栈,进程控制块。是动态的,包括创建、调度、执行和消亡。
**作用:**1,给应用提供了关键的抽象,一个独立地逻辑控制流。2,提供一个私有的地址空间,提供程序好像独占地使用内存系统的假象。

6.2 简述壳Shell-bash的作用与处理流程

**Shell:**shell本身是一个用c语言编写的程序,它交互式地解释和执行用户输入的命令,也定义各种变量和参数,并提供许多在高级语言中才具有的控制结构,包括循环和分支。它代表用户运行其他程序,是用户和系统内核沟通的桥梁。用户可以通过shell向操作系统发出请求,操作系统选择执行命令。
处理流程:

  1. 用户在命令行中键入命令
  2. Shell通过parseline builtin函数分隔命令行参数填充到参数数组构造向量。如果最后一个参数是&就在后台执行程序,否则在前台。
  3. 解析命令行后,检查第一个命令行参数是否为shell内置命令,如果是,则立即执行。
  4. 如果不是则创建子进程,在子进程中执行。如果用户要求后台操作,则返回循环顶部,等待下一个命令。否则等待作业终止。

6.3 Hello的fork进程创建过程

在终端输入 ./hello,然后enter。运行的终端程序会对输入的命令行进行解析,因为 hello 不是一个内置的 shell命令,所以解析之后终端程序判断./hello的语义为执行当前目录下的可执行目标文件 hello,之后终端程序首先会调用fork函数创建一个新的运行的子进程,新创建的子进程几乎但不完全与父进程相同:子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。

父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的逻辑控制流的指令。在子进程执行期间,父进程默认选项是显示等待子进程的完成。

6.4 Hello的execve过程

execve函数加载并运行可执行目标文件hello,且带参数列表argv和环境变量envp。这个过程是一个系统调用过程。执行一次,并且只有在调用失败时才返回。
首先加载器将可执行目标文件的代码和数据从磁盘中复制到内存,把路径拷贝到系统空间中,根据路径就可以找到文件的相应地址。
执行这个文件前会调用操作系统的代码,加载器删除子进程现有的虚拟内存段,创建一组新的代码和初始化为0的数据,堆栈,使得加载过程不需要实际从磁盘复制任何数据到内存,只需要将为原子程序的代码和数据段重新分配虚拟页,标记为无效,新的代码和数据段被初始化为可执行文件中的内容。
最后设置指针指向开始的地址,使得下一次调用该进程时,直接从入口开始执行。

6.5 Hello的进程执行

**上下文信息:**上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。
**时间片:**一个进程执行它的控制流的一部分的每一时间段叫做时间片。
hello的调度过程:

  1. 在用户模式下执行。
  2. hello从用户模式从陷入内核模式。
  3. 内核进行上下文切换,执行与hello并发的其他进程
  4. Sleep休眠时间结束
  5. 其他进程陷入内核
  6. 内核进行上下文切换,继续执行hello进程

6.6 hello的异常与信号处理

Hello程序正常执行时根据指令应该每秒输出一次Hello 7203610718 罗睿。

  1. 输入回车,程序停止一次,然后继续正常运行。
    图 46 空格结果
  2. 乱按键盘,输出所按的字母但不影响程序继续进行。
    图 47 乱按键盘结果
  3. 输入Ctrl-C信号,程序终止,hello被回收。
    图 48 Ctrl-C结果
  4. 输入Ctrl-Z信号,程序终止,hello在后台继续运行。
    图 49 Ctrl-Z结果
  5. ps jobs pstree fg kill 等命令
    图 50  ps jobs pstree指令
    图 51 fg指令
    图 52 kill指令

6.7本章小结

本章介绍了进程的概念及作用,对hello具体执行过程做了分析,讲述了Shell-bash的流程,fork创建进程,execve过程,hello异常和信号处理过程,加深了对进程的理解。

第7章 hello的存储管理

7.1 hello的存储器地址空间

**1. 逻辑地址:**逻辑地址是指由程序hello产生的与段相关的偏移地址部分。[1]
**2. 线性地址:**线性地址是一个非负整数地址的有序集合。程序hello代码中的偏移地址+相应段的基地址就生成了一个线性地址。
**3.虚拟地址:**有时也把逻辑地址称为虚拟地址。因为与虚拟内存空间的概念类似,逻辑地址也是与实际物理内存容量无关的,是hello中的虚拟地址。
**4.物理地址:**物理地址是指出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。物理地址就是程序真正在内存上运行的地址上。分页情况下,hello的线性地址会通过页目录和页表中项的计算得到hello的物理地址;不分页么hello的线性地址就是物理地址了。

7.2 Intel逻辑地址到线性地址的变换-段式管理

在段式存储管理中,程序的地址空间被划分为若干个段,每一个进程都有一个二维的地址空间。段式存储管理系统为每个段分配一个连续的分区,而进程中的各个段可以不连续的存放在内存的不同分区中。逻辑地址由段选择符和偏移量组成,线性地址为段首地址与逻辑地址中的偏移量组成。其中,段首地址存放在段描述符中。而段描述符存放在描述符表中。
图 53 段基址搜索流程图
举例说明:执行movl 8(%ebp),%eax时,逻辑地址转换为线性地址的流程为:

  1. 计算有效地址EA=R[ebp]+0*0+8。【2】
  2. 取出段寄存器DS所对应的描述符cache中的段基址(linux中为0)。
  3. 线性地址LA=段基址+EA=EA。

7.3 Hello的线性地址到物理地址的变换-页式管理

线性地址到物理地址采用分页管理的方式来完成,就是页式管理。
页表是存放在物理内存中虚拟页到物理页相映射的数据结构。CPU通过生成一个虚拟地址来访问主存,这个虚拟地址被送到内存前先转换为合适的物理地址。将虚拟地址转换为物理地址叫做地址翻译,每次地址翻译,硬件将一个虚拟地址转换为物理地址时都会读取页表。
页表就是一个页表条目(PTE)的数组。虚拟地址空间中的每个页在页表中一个固定偏移量处都有一个PTE。可以假设每个PTE是由一个有效位和一个n位地址字段组成的。有效位表明了该虚拟页当前是否被缓存在DRAM中。如果设置了有效位,那么地址字段就表示物理内存中相应的物理页的起始位置,这个物理页中缓存了该虚拟页。如果没有设置有效位,那么一个空地址表示这个虚拟页还未被分配。否则,一个非空地址指向的是该虚拟页在磁盘上的起始位置。如果页面命中,贮存直接返回PTE,如果不命中,缺页异常确定牺牲页,如果页面被修改,则把程序页面换入新的,更新PET,返回原程序,下一次操作重复。

7.4 TLB与四级页表支持下的VA到PA的变换

这个转换过程的开始和上一节中相同,即将线性地址分为虚拟页号+虚拟页偏移的形式,虚拟页号又可以拆分成TLB标记+TLB索引。当TLB不命中时,需要再页表中查询PPN,这时就需要四级页表。CR3确定第一级页表的起始地址,VPN1确定在第一级页表中的偏移量,查询出PTE,确定第二级页表的起始地址,最终在第四级页表中查询到PPN,与虚拟页偏移组合成PA。这个操作的优点是节省空间。

7.5 三级Cache支持下的物理内存访问

首先把PA分成三个部分,CT、CI、CO。
有物理地址后,首先使用物理地址的CI进行组索引,然后分别对 CT进行标志位Tag匹配。匹配成功并且块的valid标志位为1,则命中。然后根据 CO找数据偏移量取数据并返回。
若没找到Tag匹配或者标志位为0,则去下一级的cache中找,二级cache甚至三级cache中去查询数据。找到后返回结果。
在更新cache的时候,需要判断是否有空闲块。若有空闲块(即有效位valid为0),则写入;若不存在,则进行驱逐一个块。

7.6 hello进程fork时的内存映射

内存映射:Linux通过将一个磁盘上的对象映射到一个虚拟内存区域,以初始化这个虚拟内存区域的过程称为内存映射。虚拟内存可以映射到两种类型的对象。
图 54 共享对象
当fork函数被shell进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了创建一个虚拟内存给这个新进程,它创建了mm_struct,区域结构和页表的原样副本。它将两个进程的每个页面都标记为只读,同时设置每个区域结构为私有对象。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个要写私有区域的某个页面的时候,写时复制机制就会在内存中创建新页面,同时更新页表条目,然后恢复这个页面的可写权限。这个操作就可以被CPU正常执行,因此,也就为每个进程保持了私有地址空间的抽象概念。

7.7 hello进程execve时的内存映射

execve函数在当前进程中加载并运行hello需要首先删除已经存在的用户区域,也就是删除当前进程虚拟地址的用户部分中的已存在的区域结构;然后映射私有区域:创建新的区域结构为新的代码、数据、bss和栈区域。所有这些新的区域都是私有的,写时复制的。代码和数据区域分别被映射为hello文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零;程序与共享对象链接,这些都是动态链接到程序,然后把动态链表映射到用户虚拟地址空间中的共享区域内;最后execve设置当前进程上下文的程序计数器(PC),使之指向代码区域的入口点。这样下一次调度这个进程时,将可以从入口点开始执行。[3]

7.8 缺页故障与缺页中断处理

缺页故障:当指令引用一个相应的虚拟地址,而与改地址相应的物理页面不再内存中,会触发缺页故障。
首先检查虚拟地址是否合法,如果虚拟地址对所有区域都无法匹配,就返回段错误。然后查看地址的权限,判断下一个进程是否有改写这个地址的权限,如果权限不符,就是非法访问。如果只是正常缺页,则会调用异常处理程序,选择一个牺牲页,然后将他替换到物理内存中。

7.9动态存储分配管理

**基本方法:**动态内存分配器维护着一个进程的虚拟内存区域,称为堆。而分配器分为两种基本风格:显式分配器和隐式分配器。
对于每个进程,内核维护着一个变量指向堆的顶部。分配器将堆视为一组不同大小的块的集合来维护,块分为已分配的和空闲的。已分配的块显示地保留为了提供给应用程序使用。空闲的块可以用来分配,直到它被应用所分配就不再空闲。一个已分配的块保持已分配状态,直到它被释放(应用程序显示执行的或者内存分配器自身隐式执行)。
策略:
隐式分配器:
1.放置已分配的块:分配器搜索空闲链表,查找一个足够大可以放置所有请求块的空闲块。
2.分割空闲块:找到空闲块之后,确定需要分配多少空间。可以用整个空闲块,也可以把第一部分变成分配块,而剩下的变成一个新的空闲块。
3.获取额外的堆内存:如果找不到合适大小的块,分配器就会通过调用 sbrk函数,向内核请求额外的堆内存。分配器将额外的内存转化成一个大的空闲块,将这个块插入到空闲链表中,然后将被请求的块放置在这个新的空闲块中。
4.合并空闲块:空闲块会出现很多的碎片,所以需要合并。通常用边界标记(boundary tag)的方法。在每个块的结尾处添加一个脚部(footer,边界标记),其中脚部就是头部的一个副本。如果每个块包括这样一个脚部,那么分配器就可以通过检查它的脚部,判断前面一个块的起始位置和状态,这个脚部总是在距当前块开始位置一个字的距离。考虑当分配器释放当前块时所有可能存在的情况:
(1)前面的块和后面的块都是已分配的。
(2)前面的块是已分配的,后面的块是空闲的。
(3)前面的块是空闲的,而后面的块是已分配的。
(4)前面的块和后面的块都是空闲的。

显式分配器:
显式空闲链表与隐式空闲链表相比,每个空闲快里都有指向前驱和后继的指针。这使得首次适配的分配时间从块总数的线性时间减少到空闲块数量的线性时间。不过释放一个块的时间还是取决于空闲链表的块排序策略。
一种方法是后进先出的顺序维护链表,在使用边界标记的情况下,合并在常数时间内完成。另一种方法是按照地址顺序来维护链表,每个块的地址都小于其后继的地址,这种方法接近最佳适配的利用率。

7.10本章小结

本章介绍了计算机中的存储,包括地址空间的分类,地址的变换规则,虚拟内存的原理,cache的工作,和动态存储分配管理。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件
设备管理:unix io接口
所有的 IO 设备都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单低级的应用接口,称为 Unix I/O。

8.2 简述Unix IO接口及其函数

Unix I/O 接口统一操作:打开文件,改变当前文件位置,读写文件,关闭文件。
Unix I/O 函数:进程通过调用 open 函 数来打开一个存在的文件或是创建一个新文件的。flags 参数指明了进程打算如何访 问这个文件,mode 参数指定了新文件的访问权限位。fd 是需要关闭的文件的描述符,close 返回操作结果。read 函数从描述符为 fd 的当前文件位置赋值最多 n 个字节到内存位置 buf。

8.3 printf的实现分析

从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

本章主要介绍了 Linux 的 IO 设备管理方法、Unix IO 接口及其函数,分析了 printf 函数和 getchar 函数。

结论

hello的一生是这样走过的:

  1. 编写程序:程序员用高级语言写出来一个.c文件。
  2. 预处理:预处理器对它进行预处理,生成hello.i。
  3. 编译:编译器对其进行编译,生成汇编代码文件 hello.s。
  4. 汇编:汇编器对其进行汇编,生成可重定位目标程序hello.o。
  5. 链接:链接器对其进行链接,生成可执行目标程序hello。
  6. 运行:shell中输入命令。
  7. 创建子进程:shell调用fork函数,创建了新的子进程
  8. 加载:execve函数调用加载器将hello程序加载到了该子进程。这里还有虚拟内存机制的帮助,我们能够轻松地完成内存映射。
  9. 指令执行:加载器将程序计数器预置在entry point,我们的逻辑控制流可以跑起来了。
  10. 中断:内核会周期性调度进程。
  11. 访存:CPU通过MMU来访问物理地址。
  12. 动态内存分配:动态内存分配器能够动态满足进程对空间的需求。
  13. 信号:内核通过信号系统来处理程序执行中的用户请求和异常。
  14. 终止:当子进程执行完成时,内核安排父进程回收子进程,将子进程的退出状态传递给父进程。内核删除为这个进程创建的所有 数据结构。

附件

hello.c 源代码。
hello.i 预处理后的文本文件
hello.s 汇编后文本文件
hello.o 可重定位目标文件
hello 链接之后可执行文件
hello.out 反汇编之后的可重定位文件
helloo.elf hello.o的elf文件
hello.elf hello的elf文件

参考文献

[1] 逻辑地址、线性地址、物理地址和虚拟地址理解 - Red_Point - 博客园 (cnblogs.com)
[2] 段页式访存——逻辑地址到线性地址的转换 - 简书 (jianshu.com)[3] 赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).
[3] CSAPP : 内存映射 - 简书 (jianshu.com)[5] KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.
[4] 《深入理解计算机系统》中文电子版(原书第 3 版)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值