程序人生-Hello’s P2P

计算机系统

大作业

题     目  程序人生-Hello’s P2P 

专       业     计算机科学与技术   

学     号      120L022430        

班     级      2003008            

学       生      张珏            

指 导 教 师      吴锐               

计算机科学与技术学院

2021年5月

摘  要

本文主要围绕Linux系统下全周期中hello程序的执行过程,从高级语言代码hello.c开始,研究其被预处理、编译、汇编、链接并最终生成可执行文件的过程;然后,研究可执行文件执行过程中的进程管理、存储管理以及IO管理。通过对hello程序生命全周期的追踪研究,对计算机系统的各方面知识进行综合利用和回顾。

关键词:预处理;编译;汇编;链接;进程管理;存储管理;IO管理                           

目  录

第1章 概述............................................................................................................. - 4 -

1.1 Hello简介...................................................................................................... - 4 -

1.2 环境与工具..................................................................................................... - 4 -

1.3 中间结果......................................................................................................... - 4 -

1.4 本章小结......................................................................................................... - 5 -

第2章 预处理......................................................................................................... - 6 -

2.1 预处理的概念与作用..................................................................................... - 6 -

2.2在Ubuntu下预处理的命令.......................................................................... - 6 -

2.3 Hello的预处理结果解析.............................................................................. - 7 -

2.4 本章小结......................................................................................................... - 7 -

第3章 编译............................................................................................................. - 8 -

3.1 编译的概念与作用......................................................................................... - 8 -

3.2 在Ubuntu下编译的命令............................................................................. - 8 -

3.3 Hello的编译结果解析.................................................................................. - 8 -

3.4 本章小结....................................................................................................... - 12 -

第4章 汇编........................................................................................................... - 13 -

4.1 汇编的概念与作用....................................................................................... - 13 -

4.2 在Ubuntu下汇编的命令........................................................................... - 13 -

4.3 可重定位目标elf格式............................................................................... - 13 -

4.4 Hello.o的结果解析.................................................................................... - 16 -

4.5 本章小结....................................................................................................... - 18 -

第5章 链接........................................................................................................... - 19 -

5.1 链接的概念与作用....................................................................................... - 19 -

5.2 在Ubuntu下链接的命令........................................................................... - 19 -

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

5.4 hello的虚拟地址空间................................................................................ - 21 -

5.5 链接的重定位过程分析............................................................................... - 22 -

5.6 hello的执行流程........................................................................................ - 23 -

5.7 Hello的动态链接分析................................................................................ - 24 -

5.8 本章小结....................................................................................................... - 25 -

第6章 hello进程管理................................................................................... - 26 -

6.1 进程的概念与作用....................................................................................... - 26 -

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

6.3 Hello的fork进程创建过程..................................................................... - 26 -

6.4 Hello的execve过程................................................................................. - 27 -

6.5 Hello的进程执行........................................................................................ - 27 -

6.6 hello的异常与信号处理............................................................................ - 28 -

6.7本章小结....................................................................................................... - 31 -

第7章 hello的存储管理............................................................................... - 32 -

7.1 hello的存储器地址空间............................................................................ - 32 -

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

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

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

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

7.6 hello进程fork时的内存映射.................................................................. - 36 -

7.7 hello进程execve时的内存映射.............................................................. - 37 -

7.8 缺页故障与缺页中断处理........................................................................... - 38 -

7.9动态存储分配管理....................................................................................... - 39 -

7.10本章小结..................................................................................................... - 40 -

第8章 hello的IO管理................................................................................. - 41 -

8.1 Linux的IO设备管理方法.......................................................................... - 41 -

8.2 简述Unix IO接口及其函数....................................................................... - 41 -

8.3 printf的实现分析........................................................................................ - 42 -

8.4 getchar的实现分析.................................................................................... - 44 -

8.5本章小结....................................................................................................... - 44 -

结论......................................................................................................................... - 45 -

附件......................................................................................................................... - 46 -

参考文献................................................................................................................. - 47 -

第1章 概述

1.1 Hello简介

1.1.1  P2P:Program to Process

Program: C语言源程序(hello.c)。

Process: hello.c在预处理器cpp预处理下得到hello.i, 通过编译器ccl编译得到hello.s,然后通过汇编器as,得到可重定位目标程序hello.o,最后通过链接器ld得到可执行目标程序Hello。在shell中键入运行命令后,shell调用fork函数为其创建子进程(proces)。

1.1.2  020:zero to zero

Hello程序从无到有再到无的过程就是020。

第一个zero指源程序编写前什么都没有;第二个zero指源程序先经过预处理、编译、汇编、链接之后,转换为可执行文件。然后可执行文件运行过程中shell为进程映射虚拟内存、载入物理内存。CPU为进程执行逻辑控制流。当程序运行结束后,shell父进程回收 Hello 进程,内核删除相关数据,相应资源得到释放,程序结束,回到无的状态。

1.2 环境与工具

1.2.1 硬件环境:

AMD Ryzen 5 Mobile 4600H

16.00 GB RAM;842.12GHD Disk

1.2.2 软件环境

Windows10 64位;VirtualBox Manager; Ubuntu 20.04 LTS 64位;

GDB/OBJDUMP;EDB;KDD

1.2.3 开发工具

Visual Studio 2019 64位;CodeBlocks; vim; gedit+gcc;

1.3 中间结果

hello.i         预处理hello.c得到的文本文件

hello.s         编译hello.i得到的汇编文件

hello.o         汇编hello.s得到的的可重定位目标文件

hello          链接hello.o与其他组件得到的的可执行目标文件

hello.d      Hello反汇编文件

helloo.d         Hello.o反汇编文件

Hello_elf.txt     ELF格式的hello文件

Hello_o_elf.txt   ELF格式的hello.o文件

1.4 本章小结

    本章以Hello程序为例解释了p2p,020,简略描述了Hello程序的生命全周期;简述了实验环境和工具;列出了实验用到的中间结果。

第2章 预处理

2.1 预处理的概念与作用

2.1.1概念

预处理指在编译程序源代码之前,对源代码进行处理。预处理中会展开以#起始的行,试图解释为预处理指令,预处理指令一般被用来使源代码在不同的执行环境中被方便的修改或者编译。最终修改原始的C程序并生成一个文本文件。

2.1.2作用

1. 处理文件包含:将源文件中用#include 形式声明的文件包含到新的程序中,在源文件编译时,连同被包含进来的文件一同编译。

2.替换宏定义:也叫做宏展开,将宏名替换为相应字符串

3. 处理条件编译:条件编译指令如#ifdef,#ifndef,#else,#elif,#endif等,这些指令决定程序对哪些代码进行处理。预编译程序将根据信息,将不必要的代码忽视。

4.处理特殊符号:识别一些特殊的符号。例如LINE标识将被识别为当前行号,FILE则被识别为当前被编译的C源程序的名称。对于在源程序中出现的这些特殊符号将被合适的值进行替换。

2.2在Ubuntu下预处理的命令

命令:gcc hello.c -E -o hello.i

截图:

图2-1 预处理

2.3 Hello的预处理结果解析

    2.3.1结果:

                     图2-2预处理结果

2.3.2 解析:

stdio.h unistd.h stdlib.h的内容被展开,main函数的内容保持不变。

注释被删除。#define定义的宏被进行宏展开,头文件中的程序内容被包含进该文件中,如函数声明、结构体定义、变量定义、宏定义等内容。此外还有相应的符号替换等。

2.4 本章小结

本章简略阐述了预处理的概念作用,同时在虚拟机中对hello.c进行了预处理,分析了预处理结果。

第3章 编译

3.1 编译的概念与作用

3.1.1 概念

将高级程序语言书写的源程序,通过词法分析和语法分析转化为汇编语言表示的目标程序的过程。

3.1.2 作用

将高级程序语言编写的代码转变成等价的汇编代码。编译程序还具备语法检查、调试措施、修改手段、覆盖处理、目标程序优化、不同语言合用等功能。

3.2 在Ubuntu下编译的命令

命令:gcc -S hello.i -o hello.s

                图3-1编译

3.3 Hello的编译结果解析

3.3.1常量

   源程序中存在立即数,例如exit(1)中的1,如下图:

存在两个字符串常量,存储在只读数据段.rodata中,如下图

  两个字符串LC0和LC1在main中作为printf函数的参数

值得注意的是汉字被转换而字母等按原样显示

3.3.2 局部变量

   整型变量i、整型变量agrc,字符指针数组argv[]是hello.c中的局部变量

   (1) i是for循环中的循环变量。存储地址是-4(%rbp),初始为0,每次加1,跟7比较,如下图:

   (2) argc是main函数参数,表示程序运行时命令行参数个数,存储地址是-20(%rbp),如下图:

   (3) argv也是main函数参数之一,表示程序运行时命令行参数,存储地址是-32(%rbp)

  

同时argv[1],argv[2]也是printf函数参数,如下图:

3.3.3 赋值

   源程序中的赋值是for循环中的i=0,汇编代码中如下图:

   movl指数据长度为4个字节

3.3.4 算术操作

   源程序中的算术操作是i++,汇编代码中如下图:

3.3.5 类型转换

    源程序中的类型转换是sleep(atoi(argv[3]),把字符串转换成整型数,汇编代码中如下图:

   

3.3.6 关系操作

第一个关系判断是argc!=4,如下图:

 第二个关系判断是for循环中的i<8,如下

3.3.7 数组操作

     源程序中存在字符指针数组argv[],考虑到字符指针是八个字节,argv存储地址是-32(%rbp),argv[3]就应该是-32(%rbp)+24,汇编代码中也正如此:

3.3.8 控制转移

      源程序包含两处控制转移:1)对argc是否为4的if/else判断;2)对i的for循环。汇编代码中使用cmp指令与条件跳转完成if/else控制转移,同时使用“jump to middle”方式将for循环转化为while循环实现,如图所示:

3.3.9 函数操作

    (1)函数调用:调用了一次exit函数,两次printf函数,一次sleep函数,一次atoi函数和一次getchar函数

    (2)参数传递

给exit传入了1;给printf传入了argv[1]、argv[2]。给sleep函数传入了atoi(argv[3]),参数存在%rdi中;给atoi函数传入了argv[3]。

    (3)函数返回:atoi函数返回值保存在%eax。main函数最终返回0,调用了leave,ret等汇编指令,返回值0保存在%eax中。leave相当于mov esp,ebp和pop ebp。.cfi开头指令为编译器生成的调试信息。.cfi_endproc标志程序结束。如图所示:

3.4 本章小结

本章简略论述了编译的概念和作用,在虚拟机中对hello.i进行了编译,并对编译结果进行了分析解释。主要研究了编译器如何处理各种数据和操作。加深了对编译机制的理解。

第4章 汇编

4.1 汇编的概念与作用

4.1.1概念:

汇编是指把汇编语言书写的程序翻译成与之等价的机器语言程序的翻译过程。汇编程序输入的是用汇编语言书写的源程序,输出的是用机器语言表示的目标程序

4.1.2作用:

    将汇编语言转换成机器能够理解执行的二进制机器语言。生成可重定位目标文件,可以与其他文件链接。

4.2 在Ubuntu下汇编的命令

命令:gcc hello.s -c -o hello.o

                       图4-1汇编

4.3 可重定位目标elf格式

使用readelf命令生成hello.o的ELF格式文本:readelf -a hello.o > hello_o_elf.txt

4.3.1 ELF Header(ELF头)

     ELF头以一个16字节的magic序列开始:7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00。该序列描述了生成该文件的系统的字的大小和字节顺序:第五个字节标识ELF文件是是64位的(02)、第六个字节标识该ELF文件字节序是小端的(01);第七个字节指示ELF文件的版本号01;后九个字节ELF标准未做定义,一般为00。

ELF头接下来的部分帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小(64 bytes)、目标文件类型(REL可重定位文件)、系统架构(AMD X86-64)、节头部表(section headers)的文件偏移(1240 bytes),以及节头数量(14)。

                   图4-2ELF头

4.3.2 Section Headers(节头部表)

节头部表包含了文件中出现的各个节的信息,包括节的名称、类型、地址、偏移量和大小等信息。 由于是可重定位目标文件,所以每个节的地址都是0,需要重定位:先在文件头中得到节头表,然后再利用节头表中的偏移信息得到各节在文件中的起始位置。同时旗标指定了各节的操作权限。

对各节进行详细解析如下:

.text:已编译程序的的机器代码。类型为PROGBITS,即程序数据,旗标为AX,即权限为分配内存、可执行。

.rel.text:一个.text节中位置的列表,告诉链接器指令中的哪些地方需要重定位。当链接器把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。另一方面,调用本地函数的指令则不需要修改。

.data:已初始化的全局和静态C变量。类型为PROGBITS,即程序数据,旗标为WA,即权限为可分配可写。

.bss:未初始化的静态变量,以及初始化为0的全局或静态变量。类型为NOBITS,意为暂时没有存储空间,旗标为WA,即权限为可分配可写。

.rodata:只读数据,如printf语句中的格式串和开关语句的跳转表。类型为PROGBITS,即程序数据,旗标为A,即权限为可分配。

.comment节:包含版本控制信息。

.note.GNU_stack节:用来标记executable stack(可执行堆栈)。

.note.gnu.propert节:记录GNU的特有属性信息。

.eh_frame节:处理异常。

.rela.eh_frame节: .eh_frame的重定位信息。

.symtab:符号表,存储着程序中函数和全局变量的信息。

.shstrtab节:保存着各Section的名字

.strtab节:保存着程序中用到的符号的名字,每个名字都是以''结尾的字符串

                 图4-3 节头

4.3.3 .symtab(符号表)

  Ndx列是每个符号所在的Section编号,各Section的编号见Section Header Table。ABS表示不该被重定位的符号,UND是未定义符号。Value列是每个符号所代表的地址,在目标文件中,符号地址都是相对于该符号所在Section的相对地址,比如位于段的开头,地址就是0。Bind这一列可以看出符号是GLOBAL的还是是LOCAL的,GLOBAL符号是在汇编程序中用.globl指示声明过的符号。SIZE和TYPE指明符号的大小和类型。

                        图4-4符号表

4.3.4 .rela.text .rela.eh_frame(重定位节)

偏移量:需要被修改的引用节的偏移。信息:包括符号和和类型两部分,符号(symbol)标识被修改引用应该指向的符号,加数(Attend):一个有符号常数,一些重定位要使用它对被修改引用的值做偏移调整,符号名称(Name):重定向到的目标的名称。Type告知链接器如何修改新的引用:

R_X86_64_32。重定位绝对引用

R_X86_64_PC32:重定位PC相对引用

R_X86_64_PLT32: 过程链接表延迟绑定。当汇编器遇到对最终位置未定义的目标引用时,就会生成一个重定位条目,告诉链接器在合并目标文件时如何修改这个引用,重定位的条目代码段放在.rel.text中。

                       图4-5重定位节

4.4 Hello.o的结果解析

objdump -d -r hello.o > hello.d

                         图4-6反汇编

 机器语言由二进制代码表示的机器指令构成。指令的格式一般为操作码字段加地址码字段,操作码字段指明操作类型,地址码字段指明操作数或其地址。机器指令与汇编指令一一对应。

 hello.o反汇编结果文件hello.d与hello.s对比分析如下:

1.操作数表示:立即数在hello.d中以十六进制表示,在hello.s文件中以十进制表示。像立即数32在hello.d中表示为$0x20

2.分支转移:hello.d中没有hello.s中类似.L3这样的助记符,改用相对.text的偏移量来指示跳转目的地,机器指令中根据相对PC偏移量标识目的地。

3.函数调用:hello.s中call后面直接是函数名,hello.d中是PC下一条指令的地址,使得机器指令调用目标地址偏移量为0。

同时在callq指令下方给出目标函数的重定向条目,链接时再根据重定向条目补全真正的跳转地址,进行函数跳转。

4.5 本章小结

本章简略概述了汇编的概念与作用,生成了可重定位目标文件hello.o及其elf格式文本文件和反汇编文件hello.d。对elf文件进行了分段分析,同时比较了反汇编代码和汇编代码的关系和不同之处。对汇编语言、机器语言、汇编过程三者关系内容进行了一定研究。

第5章 链接

5.1 链接的概念与作用

5.1.1概念

 链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可以被加载到内存并执行。                                                                                   

5.1.2作用

执行符号解析、重定位过程,产生一个完全链接的,可以加载运行的可执行目标文件。可以独立地修改和编译大型应用分解成的小模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其它文件,使得分离编译成为可能

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

                       图5-1链接

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

使用readelf -a hello > hello_elf.txt将其ELF格式信息存储到文本文件中查看各段信息如下:

(1)ELF头:与hello.o文件ELF头有所不同:文件类型变为为EXEC(可执行文件),并且hello中给出了程序入口点地址为0x4010f0;节头数量变多,且节头部表偏移变为14208字节处

                         图5-2ELF头

(2)节头部表

                  

图5-3节头

节头部表中包含各段的基本信息,包括各段的起始地址,大小等信息

(3)可执行文件hello的elf格式文件还比hello.o的elf格式文件多了一些东西,比如程序头表、节段映射、动态节等。

5.4 hello的虚拟地址空间

使用edb加载hello,查看本进程的虚拟地址空间信息(开始于0x400000,结束于0x400ff0)。同时根据5.3中节头部表的内容对照可分段查找各段信息,以下列出几例

(1).interp段,起始地址是0x4002e0,大小是0x1c,查看时发现含有共享库/lib64/ld-linux-x86-64.so.2信息:

(2).dynstr段,起始地址是0x400470,大小是0x5c,查看发现动态链接库符号表内容

(3).rodata段:起始地址是0x402000,大小是0x3b,查看发现printf的格式串等内容

                                 图5-4虚拟地址空间

5.5 链接的重定位过程分析

5.5.1 hello和hello.o的不同

(1)hello.o反汇编文件中标注的都是相对地址,从0开始;而hello反汇编文件中已经分配好了虚拟地址。

(2)可以看到main函数部分在hello反汇编文件中并不位于虚拟地址空间的开头(0x400000),因为前面多了一些hello.d反汇编文件中没有的数据。如.init--

程序初始化执行的代码、.plt--动态链接过程链接表等。

(3)hello.o反汇编文件中某些操作数和调用的函数地址被记为0,下方有可重定位条目。hello生成后,链接器根据重定位条目进行相应处理,把确定的虚拟地址填充了进去。

通过完成符号解析和重定位等过程,链接也就完成了。

5.5.2重定位过程:

   链接生成可执行文件时,需要确定装入内存的实际物理地址,并修改程序中与地址有关的代码,这一过程叫做地址重定位。具体以下图中的重定位为例

   查看下面的可重定位条目信息,观察到类型是R_X86_64_PC32;查看hello.o的ELF格式文件的重定位节,得到有关.rodata的相关信息

r.offset = 0x1c     r.addend = -4

通过hello的ELF格式文件得到

ADDR(s) = ADDR(.text_main) = 0x401125

ADDR(r.symbol) = ADDR(.rdaota) = 0x402000

通过公式得到运行时地址:

 refaddr = ADDR(s) + r.offset = 0x401125 + 0x1c = 0x401141

然后根据重定位pc相对引用规则计算得到:

     *refptr = (unsigned) (ADDR(r.symbol) + r.addend – refaddr)

               = (unsigned) (0x402000 +4 – 0x401141)

               = (unsigned) (0xec3)

如图5.32所示,其与机器代码的反汇编结果中的0x402004一致:

5.6 hello的执行流程

                             图5-5 断点执行

在edb中设置断点,依步执行hello,记录下调用与跳转的各个子程序名或程序地址如下:

程序名称                                               程序地址

ld_2.31.so!_dl_start               0x00007ffd28f91898

ld_2.31.so!_dl_init                0x00007f2a453abc52

hello!_start                      0x4010f0

ld-2.31.so!_libc_start_main         0x401128

libc-2.31.so!__cxa_atexit           0x00007f5400007349

hello!__libc_csu_init              0x4011c0

hello!_init                       0x401000

libc-2.31.so!_setjmp               0x00007f54cab3fe22

hello!main                       0x401125

hello!printf@plt                   0x404020

hello!sleep@plt                   0x4010e0

libc-2.31.so!printf                 0x00007f3515d43ab0

libc-2.31.so!sleep                  0x00007f3515a2ced0

libc-2.31.so!exit                   0x00007f3515b345a0

5.7 Hello的动态链接分析

(1)首先简略阐述GOT和PLT定义功能:

       GOT: 一个存符号的绝对地址的数据结构,GOT表中每项保存程序中引用其它符号的绝对地址。GOT表的前三项保留,用于保存特殊的数据结构地址,其它的各项保存符号的绝对地址。GOT[1]保存的是一个地址,指向已经加载的共享库的链表地址。GOT[2]保存的是一个函数的地址,这个函数的主要作用就是找到某个符号的地址,并把它写到与此符号相关的GOT项中,然后将控制转移到目标函数。

      PLT:作用是将位置无关的函数调用转移到绝对地址。在编译链接时,链接器并不能控制执行从一个可执行文件或者共享文件中转移到另一个中,因此,链接器将控制转移到PLT中的某一项。而PLT通过引用GOT表中的函数的绝对地址,来把控制转移到实际的函数。

      (2) 在实际的可执行程序或者共享目标文件中,GOT表在名称为.got.plt的section中,PLT表在名称为.plt的section中。而根据hello ELF文件可知, GOT表起始位置为0x404000,如图;

在EDB中查看相应位置

可以观察到调用dl_start之前0x404008开始的16字节均为0,调用后再次查看相应位置,0x404008开始的16字节发生改变,说明动态链接已经发生。

    

关于变化的过程原因,以printf为例,链接阶段发现printf定义在动态库时,链接器生成一段小代码print_stub,然后printf_stub地址取代原来的printf。因此转化为链接阶段对printf_stub做链接重定位,而运行时才对printf做运行时重定位,这种动态链接过程中用到了GOT和PLT,GOT发生相应变化。

5.8 本章小结

简略描述了链接的概念作用,转换并查看了可执行文件hello的ELF格式文件,查看了hello的虚拟地址空间,对链接的重定位过程进行了分析,探究了hello的执行流程,对hello的动态链接过程进行了分析。对链接的全过程展开了初步研究并获得了一定认识。

第6章 hello进程管理

6.1 进程的概念与作用

6.1.1概念

  进程是一个执行中程序的实例,指程序的一次运行过程。更确切说,进程是具有独立功能的一个程序关于某个数据集合的一次运行活动,因而进程具有动态含义。同一个程序处理不同的数据就是不同的进程。进程是OS对CPU执行的程序的运行过程的一种抽象。进程有自己的生命周期,它由于任务的启动而创建,随着任务的完成(或终止)而消亡,它所占用的资源也随着进程的终止而释放。

6.1.2作用

  进程提供给应用程序两个关键抽象:逻辑控制流、私有地址空间。进程的引入简化了程序员的编程以及语言处理系统的处理,即简化了编程、编译、链接、共享和加载等整个过程。

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

6.2.1作用:shell 是一个交互型应用级程序,代表用户运行其他程序。shell应用程序提供了一个界面,用户通过访问这个界面访问操作系统内核的服务

6.2.2处理流程:

 (1)从终端读入输入的命令。

(2)将输入字符串切分获得所有的参数

( 3)如果是内置命令则立即执行

( 4)否则调用相应的程序执行

 (5)shell 应该接受键盘输入信号,并对这些信号进行相应处理

6.3 Hello的fork进程创建过程

用户在shell输入指令./hello,shell读取并判断输入的命令,发现hello不是内置的shell指令后调用相应程序,找到并执行当前目录下的可执行文件hello。父进程通过调用fork函数创建一个新的运行的子进程

子进程得到与父进程虚拟地址空间相同的(但是独立的) 一份副本;子进程获得与父进程任何打开文件描述符相同的副本但,子进程有不同于父进程的PID。

6.4 Hello的execve过程

子进程被创建以后,调用execve函数在当前进程的上下文中载入并运行一个新程序。函数参数filename(可执行文件),argv(参数列表),envp(环境变量列表)传入之后,execve函数加载并运行可执行文件filename(这里是hello),不存在时,execve返回并在终端显示错误信息,fork()终止后退出;存在时,execve将hello加载到子进程中,并且调用启动代码、设置新的栈结构如下,并且将控制交给hello的主函数。

                  图6-1 栈结构

6.5 Hello的进程执行

6.5.1上下文信息

  上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由一些对象的值组成,包括通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等。比如描述地址空间的页表、包含有关当前进程信息的进程表,以及包含进程已经打开文件的信息的文件表。进程的上下文分为系统级上下文(系统标识、现场、控制信息)和用户级上下文(用户堆栈、数据、程序和共享地址空间)。

6.5.2时间片

  一个进程执行它的控制流的一部分的每一时间段叫做时间片。多任务也叫做时间分片。

6.5.3用户模式和内核模式及转换

  处理器限制了一个应用可以执行的指令以及它可以访问的地址空间范围,通常使用某个控制寄存器中的一个模式位来提供这种功能,该寄存器描述了进程当前享有的特权:当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置

进程从用户模式变为内核模式的唯一方法是通过诸如中断、故障或者陷入系统调用这样的异常。异常发生时,控制传递到异常处理程序,处理器模式变为内核模式。处理程序返回到应用程序代码时,处理器变回用户模式。

6.5.4进程调度

  在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行时,我们说内核调度了这个进程。

  内核调度一个新的进程运行之后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程,上下文切换:1)保存当前进程的上下文。2)恢复某个先前被抢占的进程被保存的上下文。3)将控制传递给这个新恢复的进程。

6.5.5 hello的进程执行例子

   例如,用户模式下hello运行时调用sleep函数,陷入系统调用后变为内核模式。内核不会等待调用结束、什么都不做,而是挂起当前进程、设置定时器计时,内核进行上下文切换将当前进程的控制权交给其他进程,以用户状态运行;当定时器归零时发送一个中断信号,此时又进入内核状态执行中断处理,重新切换到hello上下文,又变为用户模式执行hello进程。

6.6 hello的异常与信号处理

(1)可能出现的异常:

中断:运行过程中可能有来自IO设备的信号。

陷阱:有意的异常,hello运行过程中可能有系统调用。

故障:hello运行过程中可能发生缺页故障。

终止:hello运行过程中可能会出现硬件错误。

(2)可能产生的信号与处理:

SIGINT     终止                     来自键盘的中断

SIGKILL    终止                     杀死程序

SIGSEGV   终止                     无效的内存引用(段故障)

SIGALARM 终止                     来自alarm函数的定时器信号

SIGCHILD  忽略(或有相应处理程序) 一个子进程停止或者终止

6.6.1正常运行结果

6.6.2运行时按ctrl+c

输入ctrl+c后,内核向前台进程组中的每个进程发送一个SIGINT信号,使其终止

6.6.2运行时按ctrl+z

输入ctrl+z后,内核向前台进程组中的每个进程发送一个SIGTSTP信号,使其停止

6.6.3乱按

  运行时输入字符,不按回车会被存到键盘缓存区里;按回车会在hello运行时换行、字符串进入stdin缓冲区,hello输出结束后由getchar处理、回车前的字符串作为输入的命令。

6.6.4运行时按ctrl+z后ps

hello只是被停止、并没有被回收,因此进程组中仍有hello进程

6.6.5运行时按ctrl+c后ps

Hello被终止后通过ps查看进程组,已经没有hello进程了

6.6.6运行时按ctrl+z后jobs

jobs命令列出当前所有作业,即hello

6.6.7运行时按ctrl+z后pstree

通过ps命令查得bash进程PID为4130,再使用pstree 4130显示bash进程拥有的进程

6.6.8运行时按ctrl+z后fg 1

输入fg 1命令,内核向后台的hello进程发送SIGCONT信号使其变回前台进程、输出剩余的字符串

6.6.9运行时按ctrl+z后kill

根据6.6.6得知hello作业号为1,输入kill -9 %1指令,内核向1号作业(hello)发送9号信号(SIGINT),使hello程序无条件终止。此时shell输出1号作业被终止前的状态,再输入ps查看进程组,被提示hello进程已经被杀死。

6.7本章小结

本章介绍了进程的概念与作用、shell的作用与处理流程;以hello进程为例,论述了fork进程创建过程、execve过程、进程执行、异常与信号处理等内容。

第7章 hello的存储管理

7.1 hello的存储器地址空间

7.11逻辑地址:也叫相对地址,汇编语言代码(访内指令)中的地址。逻辑地址是虚拟空间中相对的地址,要经过相应处理转换成线性地址、再转换成物理地址。逻辑地址可分为两部分:段选择符和段偏移量。

7.12线性地址:是逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。如果启用了分页机制,那么线性地址可以再经变换以产生一个物理地址。若没有启用分页机制,那么线性地址直接就是物理地址。例如hello中GOT表起始位置为0x404000。

7.13虚拟地址:虚拟地址在我们的书中跟线性地址是等价的。

7.14物理地址:是指出现CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。即物理内存中数据、指令存储的地址。物理地址是物理存储空间的绝对地址。

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

逻辑地址由两部分组成,段标识符和段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是段索引,表示段描述符在存储其的描述表中的位置

段索引可以理解为数组的下标,这个数组由多个“段描述符”构成,称为“段描述符表”。这样,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。

每一个段描述符由8个字节组成,这里只关心Base字段,它描述了一个段的开始位置的线性地址。

全局的段描述符,放在“全局段描述符表(GDT)”中,局部的段描述符,例如每个进程自己的,就放在“局部段描述符表(LDT)”中。什么时候用GDT,什么时候用LDT,由段选择符后三位中的的T1字段表示,T1 == 0表示用GDT,T1 == 1表示用LDT。GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。

 以下给出具体的变换过程:先给定一个完整的逻辑地址[段选择符:段内偏移地址],

1、看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。

2、拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。

3、把Base + offset,就得到要转换的线性地址了。

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

(1)页与页表

概念上而言,虚拟内存被组织为一个由存放在磁盘上的 N个连续字节大小的单元组成的数组。 磁盘上数组的内容被缓存在物理内存中 (DRAM cache) 。这些内存块被称为页。

页表是一个页表条目 (Page Table Entry, PTE)的数组,将虚拟页地址映射到物理页地址。虚拟地址空间中的每个页(VP)在页表固定位置有一个PTE ;PTE由一个有效位和一个n位地址字段组成,有效位表示虚页是否被缓存;有效位为0,地址表示VP在磁盘位置或地址为空表示虚拟页还未分配

                        图7-1页表

(2)地址翻译

虚拟地址分为两部分:虚拟页号(VPN)和虚拟页面偏移量(VPO)。物理地址同样具有两部分,物理页号(PPN)和物理页偏移量(PPO)。VPO和PPO始终保持一致,翻译物理时,有PPO=VPO。

VPO到PPO的变换过程由页表参与完成:

1.   处理器(CPU)生成一个虚拟地址(VA),将其传递给MMU,

2.   MMU 生成PTE地址发出请求,高速缓存/主存向MMU返回PTE

PTE中存储着每个虚拟页号对应的物理页号。

3. MMU 将物理地址传送给高速缓存/主存

4.高速缓存/主存返回所请求的数据字给处理器

当发生缺页异常时:

3. PTE有效位为零, 因此 MMU 触发缺页异常

4. 缺页处理程序确定物理内存中牺牲页 (若页面被修改,则换出到磁盘)

5. 缺页处理程序调入新的页面,并更新内存中的PTE

6.缺页处理程序返回到原来进程,再次执行导致缺页的指令

                          图7-2虚拟地址

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

(1)TLB

TLB:页表的缓存,MMU中一个小的具有较高相联度的缓存,实现虚拟页面向物理页面的映射,对于页面数很少的页表可以完全包含在TLB中。

MMU 使用虚拟地址的 VPN 部分来访问TLB,分为两部分:TLB标记(TLBT)和TLBT索引(TLBI)。如果想查阅的PTE在TLB中,就能节省一定的周期。

             图7-3TLB

(2)多级页表

存储全部页表所需空间过大,常采取多级页表的方式。

一级页表中的每个 PTE 指向一个页表 (常驻内存),而其他级别的页表在需要时才进行创建、 调入或调出(这就节省了空间),最后一级页表中的每个PTE包含某个物理页面的PPN,或者一个磁盘块的地址。

               图7-4多级页表

以四级页表为例,36位虚拟地址被寄存器划分组成四个9位的片,每个片被寄存器用作一个页表中偏移量。CR3寄存器储存了一个L1页表的一个物理起始基地址,。VPN1提供了到一级页表的偏移量,相应PTE指向一个二级页表。VPN2则提供了这个二级页表的偏移量,总共四级、以此类推,除了最后一级页表PTE包含的是PPN。

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

7.5.1组成

得到物理地址后,MMU发送物理地址给缓存,缓存把物理地址被拆分为三部分:

1.缓存偏移(CO):位于特定组下的第几块。CI与CO组成PPO。

2. 缓存标记(CT):每行有相应标记位,标记与CT相匹配才能命中。CT与PPN相同

3.缓存组索引(CI):表示位于cache中第几组。

7.5.2访问流程

1.根据CI找到相应的缓存组

2.根据CT与找到的缓存组的标志位比较,若相同且有效位为1则命中

3.根据CO在缓存组内找到对应数据字节,返回给MMU,再传递回CPU

4.如果不命中,则向更低一级的cache请求所需块,然后将这个新块在原来找到的位置保存在一个行中(根据策略寻找位置)。

                图7-5地址对应

7.6 hello进程fork时的内存映射

7.6.1内存映射

Linux通过将虚拟内存区域与磁盘上的对象关联起来以初始化这个虚拟内存区域的内容.  这个过程称为内存映射(memory mapping)。

7.6.2进行fork时的操作

hello进程fork时为新进程创建虚拟内存 创建当前进程的的mm_struct, vm_area_struct和页表的原样副本. 两个进程中的每个页面都标记为只读。两个进程中的每个区域结构(vm_area_struct)都标记为私有的写时复制(COW)。在新进程中返回时,新进程拥有与hello进程相同的虚拟内存、映射了同一个共享对象。随后的写操作通过写时复制机制创建新页面。

7.6.3写时复制机制

写私有页的指令会触发保护故障,而故障处理程序创建这个页面的一个新副本,故障处理程序返回时会重新执行写指令。(创建副本会被尽可能地延迟)

                     图7-6共享对象

7.7 hello进程execve时的内存映射

假设hello进程执行了如下的execve调用:execve(“a.out”,NULL,NULL),那execve函数会在hello进程中加载并运行包含在可执行目标文件a.out中的程序,用a.out程序有效地替代了当前程序。加载并运行a.out需要以下几个步骤:

1)删除已存在的用户区域。删除hello进程虚拟地址地用户部分中地已存在的区域结构。

2)映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为a.oiut文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在a.out中。栈和堆区域也是请求二进制零的,初始长度为零。

3)映射共享区域。如果a.out程序与共享对象(或目标)链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。

4)设置程序计数器(PC)。execve做的最后一件事情是设置hello进程上下文中的程序计数器,使之指向代码区域的入口点。

                  图7-7内存映射

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

7.8.1缺页故障

缺页:虚拟内存中的字不在物理内存中(DRAM缓存不命中)

7.8.2缺页中断处理

1.缺页导致页面出错 ,引起缺页异常,缺页异常处理程序被调用

2.选择一个牺牲页处理程序决定物理内存中的牺牲页,如果这个页是非空的就把它换到磁盘。

3.处理程序调入新页面并更新PTE。

4.处理程序返回,程序继续执行触发缺页异常的那条指令。

7.9动态存储分配管理

7.9.1动态内存分配器

动态内存分配器维护一个进程的虚拟内存区域,称为堆。不失一般性地,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向更高的地址生长。对于每个进程,内核维护一个变量brk,它指向堆的顶部。

分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显示执行的,要么是内存分配器自身隐式执行的。

分配器有两种基本风格。两种风格都要求应用显式地分配块。它们的不同之处在于由哪个实体来负责释放已分配的块。

显示分配器,要求应用显式的释放任何已分配的块。例如,C标准库提供一种叫做malloc程序包的显式分配器。C程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块。C++中的new和delete操作符与C中的malloc和free相当。

隐式分配器,另一方面,要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器,而自动释放未使用的已分配的块的过程叫做垃圾收集。注入Lisp、ML以及Java之类的高级语言就依赖垃圾收集来是释放已分配的块。

7.9.2malloc

C标准库提供了一个称为malloc程序包的显式分配器,程序通过调用malloc函数从堆中分配块。

其作用是在内存的动态存储区中分配一个长度为size的连续空间。此函数的返回值是分配区域的起始地址,或者说,此函数是一个指针型函数,返回的指针指向该分配域的开头位置。

如果分配成功则返回指向被分配内存的指针(此存储区中的初始值不确定),否则返回空指针NULL。当内存不再使用时,应使用free()函数将内存块释放。函数返回的指针一定要适当对齐,使其可以用于任何数据对象

一般它需和free函数配对使用。free函数能释放某个动态分配的地址,表明不再使用这块动态分配的内存了,实现把之前动态申请的内存返还给系统。

7.9.3隐式空闲链表和显式空闲链表

(1)隐式空闲链表的空闲块是通过头部中的大小字段隐含地连接着的。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。我们需要某种特殊标记的结束块,即一个设置了已分配位而大小为零的终止头部。

用隐式空闲链表来组织堆。阴影部分是已分配块,没有引用的部分是空闲块。头部标记为(大小(字节)/已分配位)

(2)显式空闲链表是将空闲块组织为某种形式的显式数据结构。堆被组织为一个双向空闲链表,在每个空闲块中,都包含一个前驱和后继的指针。使用双向链表而不是隐式空闲链表使得首次适配的时间从块数的线性时间减小到空闲块数的线性时间。

7.10本章小结

本章主要是涉及hello运行过程中的存储管理。简述了hello的存储器地址空间中逻辑地址、线性地址、虚拟地址、物理地址的相关概念;结合hello的运行分析了Intel的段式管理与页式管理,还有TLB与页表支持下的地址变换、三级cache支持下的物理内存访问;对hello进程fork与execve时的内存映射以及缺页故障及其处理进行了讨论分析;并简要阐述了动态存储分配管理的内容类别。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

设备管理:unix io接口

所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单低级的应用接口,称为Unix I/O。

8.2 简述Unix IO接口及其函数

8.2.1接口

(1)打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想 要访问一个 I/O 设备,内核返回一个小的非负整数,叫做描述符,它在 后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文 件的所有信息。

(2)Shell创建的每个进程都有三个打开文件:标准输入,标准输出,标准错误。

(3)改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行 seek,显式地将改变当前文件位置k。

(4)读写文件:一个读操作就是从文件复制 n>0 个字节到内存,从当前文 件位置 k 开始,然后将 k 增加到k+n,给定一个大小为m字节的而文件,当k>=m 时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。

(5)关闭文件:内核释放文件打开时创建的数据结构,并将这个描述符恢 复到可用的描述符池中去。

8.2.2函数

(1)int open(char* filename,int flags,mode_t mode),open函数打开地址为filename的一个文件,若不存在,则创建一个新文件。然后返回描述符。描述符总是当前可用描述符池中最小的那个。flags参数指明了进程打算如何访 问这个文件,mode参数指定了新文件的访问权限位。

(2)int close(fd),fd是需要关闭的文件的描述符,close 返回操作结果。

(3) ssize_t read(int fd,void *buf,size_t n),read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。

(4)ssize_t wirte(int fd,const void *buf,size_t n),write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置

(5)lseek函数可以显式改变文件当前位置。

8.3 printf的实现分析

先给出printf函数:

形参列表的“...”是可变形参的一种写法。当传递参数的个数不确定时,就可以用这种方式来表示。很显然,我们需要一种方法,来让函数体可以知道具体调用时参数的个数。

va_list:typedef char *va_list, 这说明它是一个字符指针。其中的: (char*)(&fmt) + 4) 表示的是...中的第一个参数。

                        图8-1 vsprintf

vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。vsprintf返回的是一个长度,即打印出来的字符串的长度。

int INT_VECTOR_SYS_CALL表示要通过系统来调用sys_call这个函数,可以认为其功能是显示格式化了的字符串。

执行write后,标准输出文件中存储着待输出字符的ascii码。字符显示驱动子程序将通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中。显示芯片会按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量),实现打印效果。

8.4 getchar的实现分析

给出getchar函数

                        图8-2 getchar

当某个程序调用getchar函数时,程序等待用户按键,用户按的时候,键盘接口获得一个键盘扫描码,此时同时产生一个中断的请求,系统调用键盘中断处理子程序,接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。之后,getchar从标准输入流STDIO中读入一个字符,并据此设定返回值,决定是否将用户输入的字符回显到屏幕。如果用户在键入回车符前输入多个字符,其余字符仍然保留在键盘缓冲区中,等待后续处理。

8.5本章小结

本章简介了Linux的IO设备管理方法与Unix IO接口及其函数,并分析了printf和getchar的实现,对hello的IO管理相关知识进行了一定介绍。

结论

过程

1. 预处理hello.c得到hello.i

2. 编译hello.i得到到hello.s

3.汇编hello.s得到可重定位目标文件hello.o

4.链接hello.o得到可执行文件hello

5运行:在shell中输入./hello

6.shell调用fork函数,生成子进程;

7.execve函数加载运行当前进程的上下文中加载并运行新程序hello

8.存储管理:用页表、TLB等结构协助管理存储系统,同时进行内存映射等。

9.信号管理:对 Ctrl+c、 Ctrl+z等信号进行管理

10.IO管理,管理IO设备、利用IO接口等。

13终止:hello进程执行完成时,内核安排父进程回收子进程,删除为这个进程创建的所有数据结构。

感想

         抽象与具体。计算机系统中有许多抽象,如文件是对IO设备的抽象,虚拟内存是对物理存储的抽象等,抽象基于具体,又区别于具体。通过对抽象的具体实现,我们构建了更易理解和操作的系统。

         整体与部分。程序的一生可以分为许多部分,但孤立的部分之间又有联系,有的设计单从某一部分看来是没有必要的,但能方便下一部分的进行,使整个计算机系统称为有机的整体。

        

附件

列出所有的中间产物的文件名,并予以说明起作用。

hello.c

源代码

hello.i

hello.c预处理后产生的文本文件

hello.s

hello.i程序编译产生的汇编代码

hello.o

hello.s程序汇编产生的可重定位目标文件

hello.d

hello.o的反汇编文件

hello

hello.o经过链接后的可执行文件

helloo.d

hello的反汇编文件

hello_elf

helloELF格式文件

hello_o_elf

hello.oELF格式文件

ICS大作业论文.docx

ICS大作业论文.pdf

CSDN截图

参考文献

[1]  大卫. 深入理解计算机系统[M]. 北京:机械工业出版社,2019.6.