程序人生-Hello’s P2P

 

计算机系统

 

大作业

计算机科学与技术学院

2021年5月

摘  要

Hello.c作为每一个程序员刚开始编程时就接触的程序,它的一生也是丰富曲折的。本文主要介绍hello.c从C语言程序经过预处理、编译、汇编、链接生成可执行目标文件hello的过程,以及它的进程管理、存储管理和IO管理。并且,旨在通过hello程序的一生,让读者从程序员的角度对计算机系统有更深刻的了解。

关键词:计算机系统;P2P;020;                       

目  录

第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。首先,我们程序员编写hello.c,此时hello是一个程序(program),为了在系统上运行hello.c程序,hello.c首先经过预处理器(cpp)得到预处理后的源程序hello.i;接着,编译器(cc1)将其翻译为汇编程序hello.s;然后经过汇编器(as)翻译成机器语言指令,把这些指令打包成可重定位目标程序hello.o;接下来,经过链接器,将调用的标准C库中的函数对应的预编译好了的目标文件以某种方式合并到hello.o文件中,最终得到可执行目标程序hello。最后,在终端中./hello后,操作系统fork一个子进程hello来执行这个程序,execve加载这个程序,我们的hello就成为了一个进程(process)。

020,即From Zero-0 to Zero-0。CPU通过取指、译码、执行、访存、写回、更新PC等执行hello程序。首先需要将虚拟地址转换为物理地址,通过TLB和页表加速地址的翻译,高速缓存cache加快数据的传输。IO管理和Shell的进程处理和信号处理机制可以处理hello运行过程中的信号。当进程结束时,shell会回收hello进程,并且内核会从系统中删除hello所有痕迹。这就是O2O的过程。

1.2 环境与工具

1.2.1 硬件环境

2.3 GHz 八核Intel Core i9;16 GB 2667 MHz DDR4

1.2.2 软件环境

Ubuntu 20.04.4;(在Mac电脑上安装的虚拟机)

1.2.3 开发与调试工具

gcc;gedit;edb;readelf;objdump

1.3 中间结果

hello.i:展示hello.c经过预处理的结果。

hello.s:展示hello.i经过编译的结果。

hello.o:展示hello.s经过汇编的结果。

hello:hello.o与其他可重定位目标文件链接生成的可执行文件

1.4 本章小结

本章简略介绍了hello程序的一生以及撰写所用的环境与工具。

一个凝结了现代计算机科学家智慧的伟大结晶即将诞生。

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

预处理的概念:程序设计领域中,预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。典型地,由预处理器(preprocessor) 对程序源代码文本进行处理,得到的结果再由编译器核心进一步编译。这个过程并不对程序的源代码进行解析,但它把源代码分割或处理成为特定的单位——(用C/C++的术语来说是)预处理记号(preprocessing token)用来支持语言特性(如C/C++的宏调用)。[1]

预处理的作用:预处理指令一般被用来使源代码在不同的执行环境中被方便的修改或者编译。预处理中会展开以#起始的行,试图解释为预处理指令(preprocessing directive),其中,主要包括:

  1. 宏定义(#define):#define指令定义了一个宏---用来代表其他东西的一个命令,通常是某一个类型的常量。预处理会通过将宏的名字和它的定义存储在一起来响应#define指令。当这个宏在后面的程序中使用到时,预处理器“扩展”了宏,将宏替换为它所定义的值。
  2. 文件包含(#include):#include指令告诉预处理器打开一个特定的文件,将它的内容作为正在编译的文件的一部分“包含”进来。例如:命令:#include<stdio.h>指示预处理器打开一个名字为stdio.h的文件,并将它的内容加到当前的程序中。
  3. 条件编译(#if/#ifdef/#ifndef/#else/#elif/#endif):在预处理时,按照不同的条件去编译程序的不同部分,从而得到不同的目标代码。例如,#if,#ifdef,#ifndef,,#elif,#else 和#endif 指令可以根据编译器可以测试的条件来将一段文本包含到程序中或将不必要的代码排除在程序之外。

除此之外,预处理中,还会展开:#line(行控制)、#error(错误指令)、#pragma(和实现相关的杂注)以及单独的#(空指令)(删除注释代码)等指令。

2.2在Ubuntu下预处理的命令

命令:cpp hello.c  > hello.i

图 2-1-1 Ubuntu下预处理hello.c

2.3 Hello的预处理结果解析

直接打开hello.i文件首先可以发现,hello.i依旧是一个文本文件的形式,里面的代码和c语言的代码类似,但长度由23行变成了3060行。

首先,通过观察hello.i的最后部分,如图2-3-1所示,我们可以发现从hello.i中的3047行开始就是hello.c文件中第10行以后的内容,说明预处理器不会对main函数进行修改。

在观察hello.i前面的部分,如图2-3-2所示。由于hello.c中前面几行有用//注释掉的部分,如图2-3-3所示。在预处理的过程中,预处理器会直接删除用//注释掉的代码,因此hello.i中并没有有关于这几行代码的内容。根据上文所述,再看hello.c中有如下三条预处理指令:

#include <stdio.h>

#include <unistd.h> 

#include <stdlib.h>

这三条预处理指令对应上文中介绍的文件包含(#include)。根据这三条预处理指令,预处理器会读取头文件stdio.h、unistd.h、stdlib.h,并把它们加到当前的程序中,主要包括结构体的定义(typedef struct)、函数声明(extern)、对引用目录的标注(# 28 “/usr/include/stdio.h” 2 3 4)等内容。若源程序中有#define预处理指令还会对相应的符号进行替换,或者其他类型的预处理指令,预处理器也会执行对于的操作。也正是因为读取了三个头文件,并将它们加到了hello.i中,程序增加了三千多行代码。

 

图2-3-1 hello.i的最后部分

图2-3-2 hello.i的前面部分

 

图2-3-3 hello.c内容

2.4 本章小结

本章主要介绍了预处理的概念与作用、通过在Ubuntu下预处理的命令“cpp hello.c > hello.i”,将hello.c经预处理输出生成hello.i文件,并简单地对hello.i的内容进行了解析。

至此,我们的Hello程序才刚刚从母亲(程序员)的孕育中诞生,还是一个啼哭的婴儿,但是,比较完整地保全了它被塑造时的原样(hello.c),迎接它的还有漫长而曲折的人生。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

编译的概念:编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序的过程。[2]

编译的作用:将经过预处理得到的预处理文件翻译为汇编语言格式的汇编文件(.s)。它为不同高级语言的不同编译器提供了通用的输出语言,方便汇编器将其翻译为机器语言指令。同时,编译器将会检查语法是否存在错误。并且,生成的汇编语言程序(.s) 既比预处理文件(.i)更容易让机器理解、又比可重定位目标文件(.o)更容易让程序员理解。

注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序

3.2 在Ubuntu下编译的命令

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

 

图3-2-1 Ubuntu下编译hello.c

3.3 Hello的编译结果解析

首先,如图3-3-1所示,为编译产生的汇编语言文件hello.s的内容。

图3-3-1 hello.s的内容

3.3.1 数据

  1. 常量

如图3-3-2所示,程序中有两个字符串,由图中红色下划线指出,这两个字符串均为函数printf()的参数。编译器将它们放到LC0和LC1中。根据UTF-8的编码原则,汉字被编码为三个字节,在hello.s中以编码形式保存;而英文、\n、%、空格等字符串,则按照ASCII码的规则在hello.s中进行编码、保存。

 

图3-3-2 hello.c中的常量

  1. 变量(全局/局部/静态)

如图3-3-3所示,hello.c中用来记录循环次数多整型局部变量i被存在栈中,用rbp相对寻址来访问存储该局部变量的地址,即-4(%rbp),占用4个字节的栈空间。

 

图3-3-3 hello.c中的变量

3.3.2 赋值

如上图3-3-3所示,使用movl    $0, -4(%rbp)语句的,将i的值赋为0。因为变量i占4个字节,因此,用指令movl进行赋值。

3.3.3 类型转换

hello.c中涉及的类型转换是:atoi(argv[3]),如图3-3-4可以看出,在hello.s中,直接call atoi@PLT函数来实现ASCII码到int型整数的类型转换。

 

图3-3-4 类型转换

3.3.4 算数操作

如图3-3-3所示,在源程序hello.c的for循环中,i++语句经过编译后,通过操作addl      $1, -4(%rbp)来完成。addl就是将前一个数加到后一个参数上,此处是加到地址-4(%rbp)存储的数上。

3.3.5 关系操作

如图所示,在hello.c中进行了两次关系操作。首先是判断argc!=4,如图3-3-5所示,该操作由指令cmpl $4, -20(%rbp)完成,cmpl是将两个参数进行比较,根据比较的结果设置标志位,若标志位符合后面语句je跳转的条件,即argc!=4,则会跳转到L2对应的语句开始执行。

然后是判断i<8,如图3-3-6所示,该操作由指令cmpl     $7, -4(%rbp)完成,同样是比较两个参数,然后设置条件位,若i<=7成立的话,我们执行接下来的指令jle .L4,返回循环的开头进行新一轮循环。jle是条件跳转指令,当且仅当标志寄存器(SF^OF)|ZF为1时进行跳转。而若cmpl得到小于等于的结果,它设置的标志寄存器值恰好满足这个跳转条件。

 

                图3-3-5  argc!=4                                               图3-3-6 i<8

3.3.6 数组操作

如图3-3-6所示,在hello.c中,有数组char *argv[],该数组是一个指针数组,里面存储的是指向字符串的首地址的指针,并且在64位下,指针的大小是8个字节。因此访问数组的第i个元素的地址,只需要将数组起始地址加上8*i即可,之后使用括号取地址指向的值就是argv[i]的值。又因为char *argv[]是main()函数的第二个形参,所以应该存储在寄存器rsi中。又由图3-3-6可知,指令movq   %rsi, -32(%rbp)将该数组的首地址存在了栈中,-32(%rbp)的位置。因此在图3-3-7中,可见先将数组起始地址加上了16 = 8 * 2,访问argv[2],将该地址处的值取出来,得到argv[2]的值存在rdx中。同理,将argv[1]存在寄存器rsi中。分别以argv[1]和argv[2]为第二、三个参数调用函数printf@PLT。同理,又取出argv[3]存入rdi中,作为第一个参数,调用函数atoi@PLT,然后又将该函数存在rax中的返回值,放入rdi中,作为第一个参数调用函数sleep@PLT。

图3-3-7 将数组存入栈中

 

图3-3-8 argv数组的各元素读取

3.3.7 控制转移

如前面的图3-3-5所示,对于if(argc!=4)语句,若argc!=4成立,则跳转到L2语句,此处因为没有else语句,所以如果不成立,则跳过跳转指令,继续按顺序执行。

如图3-3-6所示,对于for(i=0;i<8;i++)语句,首先,使用movl $0, -4(%rbp)对i赋值0。然后我们使用cmpl $7, -4(%rbp)执行对i<8的判断,。若i<=7成立的话,跳转到L4,即返回循环的开头,执行i++更新循环条件,进行新一轮循环,否则循环结束,接着执行for循环之后的代码。

3.3.7 函数调用

如图3-3-7所示,由3.3.6中的分析,对于要调用的printf函数,格式串是第一个参数,存储在rdi中。先读取格式串,然后以argv[1]和argv[2]为第二、三个参数调用函数printf@PLT。同理,取出argv[3]存入rdi中,作为第一个参数,调用函数atoi@PLT,然后又将该函数存在rax中的返回值,放入rdi中,作为第一个参数调用函数sleep@PLT。

由于此程序中调用的函数均为库文件中的函数,我们无法具体分析函数里面究竟进行了怎样的操作。但根据我们这学期所学习的知识,我们依然可以推测,调用一个函数,应首先将函数的返回地址压栈,然后跳转到函数的代码处执行。函数代码执行结束之后,我们要返回调用这个函数的下一条语句继续执行,这也就是之前压栈的返回地址,因此我们执行ret操作,将栈中的返回地址pop出来,就能跳转到返回地址处了。

3.4 本章小结

本章主要介绍了编译的概念与作用,通过在Ubuntu下编译命令“gcc -S hello.c -o hello.s”,将hello.c(或hello.i)经编译生成汇编语言文件hello.s文件,并简单地对hello.s的内容进行了解析。

至此,我们的Hello程序从高级语言转换成了汇编语言,每条语句都以一种文本格式描述了一条低级机器语言指令。已经变成了一个呀呀学语的孩童,正在模仿大人说话。但此时,我们的CPU还无法“听懂”他在说什么。因此,这个呀呀学语的孩童——我们的Hello程序,还有待进一步获得语言的能力。

(第32分)

第4章 汇编

4.1 汇编的概念与作用

       汇编的概念:汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序(relocatable object program)的格式,并将结果保存在目标文件hello.o中。

       汇编的作用:将hello.s代码转化为机器可以直接处理的二进制表示的机器指令,能够被CPU直接读取并执行相应指令,以便后序链接生成可执行文件。

注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。

4.2 在Ubuntu下汇编的命令

       命令:as hello.s -o hello.o

 

图4-2-1 Ubuntu下对hello.s汇编

4.3 可重定位目标elf格式

4.3.3 hello.o的ELF格式

首先,典型的EFL格式如图4-3-1所示。

 

图4-3-1 典型的EFL格式

EFL头(EFL header)以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF 头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括 ELF 头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量。

夹在 ELF 头和节头部表之间的都是节。

一个典型的 ELF 可重定位目标文件包含下面几个节:

.text:已编译程序的机器代码。

.rodata:只读数据。

.data:已初始化的全局和静态 C 变量。

.bss:未初始化的全局和静态C 变量,以及所有被初始化为0的全局或静态变量。在目标文件中这个节不占据实际的空间,它仅仅是一个占位符。目标文件格式区分已初始化和未初始化变量是为了空间效率:在目标文件中,未初始化变最不需要占据任何实际的磁盘空间。运行时,在内存中分配这些变量,初始值为0。

.symtab:一个符号表,它存放在程序中定义和引用的两数和全局变量的信息。

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

.rel.data:被模块引用或定义的所有全局变量的重定位信息。一般而言,任何已初始化的全局变量,如果它的初始值是一个全局变量地址或者外部定义两数的地址,都需要被修改,

.debug:一个调试符号表,其条目是程序中定义的局部变量和类型定义,程序中定义和引用的全局变量,以及原始的C 源文件。

.line:原始C 源程序中的行号和.text 节中机器指令之间的映射。

strtab:一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部中的节名字。

节头部表:不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的条目(entry)。

       4.3.2 用readelf等列出其各节的基本信息

              如图4-3-2所示。

 

图4-3-2 hello.o各节的基本信息

       4.3.3 重定位项目分析

              如图4-3-3所示,为hello.o可重定位段段信息。

 

图4-3-3 hello.o可重定位段信息

图中,offset 是需要被修改的引用的节偏移。symbol标识被修改引用应该指向的符号,Sym.Value代表该符号的值,Sym.Name则代表该符号的名称。type 告知链接器如何修改新的引用。addend 是一个有符号常数,一些类型的重定位要使用它对被修改引用的值做偏移调整。

首先,由图中的提示我们可以看出,重定位段.rela.text在节偏移为0x388的位置包含了8个重定位条目,而重定位段.rela.eh_frame在节偏移为0x448的位置包含了1个重定位条目。

通过我们本学期的学习,我们已经知道我们知道R_X86_64_PC32类型代表着重定位一个使用32位PC相对地址的引用,R_X86_64_PLT32重定位类型代表着使用PLT进行重定位。

因此,我们可以通过这个重定位段,得到各个重定位条目的信息,例如:符号.rotate开始于节偏移0x1c的地方,用32位PC相对地址的引用来进行重定位,并且要对这个被修改引用的值进行-4的偏移调整。对于其他符号的分析与之类似,故省略。

4.4 Hello.o的结果解析

对hello.o进行反汇编的结果如图4-4-1所示。

 

图4-4-1 hello.o的反汇编结果

将其与图3-3-1中的hello.s进行比较,我们可以发现两者有如下不同。

1首先,最明显的不同在于在反汇编的结果中,最左边一列是每一条汇编语言所对应的二进制的机器指令,这样的机器语言与汇编语言是一一对应的。而在hello.s中没有这个内容。2而且反汇编器使用的指令命名规则与GCC生成的汇编代码使用的有些细微的差别:反汇编代码中省略了汇编代码中很多指令结尾的“q”等后缀,例如movq、pushq等。相反,反汇编代码中部分指令例如call、leave和ret指令添加了“q”后缀,而这些指令的后缀在汇编代码中是省略的。3在反汇编的结果中,列出了需要重定位的段或符号的重定位条目。4对于操作数的表示,hello.s中数字以十进制表示,而反汇编文件中以十六进制表示5对于分支转移函数的调用,在反汇编的文件中,jmp指令采用相对地址偏移的寻址方式进行跳转,而hello.s则直接利用标号.L0等助记符来标识进行跳转,更便于阅读。

每一个机器语言指令由两部分构成,一个是操作码,一个是操作数。操作码规定了指令的操作,是指令中的关键字,不能缺省。操作数表示该指令的操作对象。[3]对于不同的汇编语言指令,与之对应的机器语言是不同的。首先,机器语言的高8位对应与不同的操作指令。例如,jle的机器码是7e,je的机器码是74,它们都是跳转指令因此高4位相同,而又因为功能不同,低4位不同。而操作码之后的部分则对应于操作数。例如83 7d ec 04          cmpl   $0x4,-0x14(%rbp)中,ec表示-0x14的补码,04则表示0x4。而由于不同指令、不同操作数所占字节数不同,并且有可能有附加的寄存器指令符字节,所以,不同的指令对应的机器代码的长度是不同的。

4.5 本章小结

在本章中,介绍了汇编的概念及作用,并且对于生成的可重定位目标文件hello.o的EFL格式进行了分析,还利用objdump对其进行了反汇编,比较并分析了反汇编的结果与hello.s的不同之处。

这时,我们的hello.c程序已经学会具有了语言能力——与计算机进行交流的能力。它通过二进制的机器指令,能够被CPU直接读取并执行相应操作。这也就意味着,它马上就可以进入到更广阔的“社会”中,去和形形色色的程序进行交流了。

(第41分)

5章 链接

5.1 链接的概念与作用

链接的概念:链接(linking)是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。[2]

链接的作用:链接将多个可重定位文件整合,生成可执行文件。并且,使得分离编译成为可能,这样,用户们在编写大型项目时,不用将整个文件放在一个巨大的源代码内,而是可以将其分为更小的模块,当我们需要修改它时,只需要对其需要修改的特定部分操作,并在修改后重新进行编译和链接即可。

注意:这儿的链接是指从 hello.o 到hello生成过程。

5.2 在Ubuntu下链接的命令

ld  -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 /usr/lib/gcc/x86_64-linux-gnu/9/crtbegin.o hello.o -lc /usr/lib/gcc/x86_64-linux-gnu/9/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -z relro -o hello

图5-2-1 对hello.o进行链接

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

如图5-3-1所示,为一个典型的ELF可执行目标文件格式。

 

图5-3-1 典型的ELF可执行目标文件格式

可执行目标文件的格式类似于可重定位目标文件的格式。ELF 头描述文件的总体格式。它还包括程序的入口点(entry point),也就是当程序运行时要执行的第一条指令的地址。.text、.rodata 和.data 节与可重定位目标文件中的节是相似的,除了这些节已经被重定位到它们最终的运行时内存地址以外。init 节定义了一个小两数,叫做init,程序的初始化代码会调用它。因为可执行文件是完全链接的(已被重定位),所以它不再需要.rel 节。

用readelf指令列出各段的基本信息,如图5-3-2所示。根据section headers,我们可以知道所有段的基本信息,如Address是各段的起始地址,Entsize是各段段大小。

 

图5-3-2 hello中各段的基本信息

5.4 hello的虚拟地址空间

使用edb加载hello,如图5-4-1所示,可以看到,hello的虚拟地址空间起始于0x400000。

图5-4-1 hello的虚拟地址空间   

与5.3中的节头部表进行对照,可以得到各个段在虚拟内存中的位置。由图5-3-2可知,.init段在虚拟地址中的起始地址为0x401000,则可在edb中找到.init段对应的位置,如图5-2-2所示。其他各段信息同理可得。

 

图5-4-2 .init段的信息

5.5 链接的重定位过程分析

  

图5-5-1 用hello反汇编结果

链接的主要过程分为:符号解析和重定位。如图5-5-1所示,对比通过hello和hello.o进行反汇编的结果,我们可以发现以下不同:1在hello反汇编的结果中,汇编语言按照不同的函数将代码分成了一段一段的,并且增加了很多函数,如<__libc_csu_fini>、<__libc_csu_init>等,而在hello.o的反汇编中,只有<main>函数的汇编代码。说明此时已经完成了链接,即已经将不同的库中的程序所需的的函数链接到了可执行文件hello中了。2在hello的反汇编结果中,每一条汇编指令前都有了该指令对应的虚拟地址,并且从0x400000开始,而在hello.o中从0x0开始。3相比于hello.o,hello的反汇编中,没有了重定位的条目,因为此时已经完成了重定位。4之前hello.o的反汇编中call指令,调用需要重定位的函数时,地址的机器码都暂时填充了0,而在汇编代码中,用的是下一条指令相对于main函数的地址偏移量来表示的,而在hello中机器代码中填写了重定位后的偏移量、汇编代码填写了函数的虚拟地址。

重定位由两步组成:重定位节和符号定义,重定位节中的符号引用。在重定位节和符号定义中,链接器将所有相同类型的节合并为同一类型的新的聚合节。例如,所有输入模块的.data节被全部合并成hello的.data节。然后,链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。在重定位节中的符号引用中,链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。如图5-5-2所示,调用atoi函数时,机器指令e8后面并没有跟上一个正确的地址,而是用一个重定位条目来告诉链接器这个函数在链接时需要进行重定位。重定位后链接器使atoi这个符号指向了0x4010c0这个地址,并且修改了代码节中的代码: e8 75 fe ff ff,如此完成了重定位。

图5-5-2 重定位后的atoi

例如,我们通过前面的elf可以发现,.rodata段段type是R_X86_64_PC32,即使用32位PC相对地址的引用,故我们可以通过书上给出的PC相对引用的方法找到重定位后.rodata段的PC相对地址。

*refptr = (unsigned)(ADDR(.rodata)+addend - ADDR(main)- Offset)

  =(unsigned)(0x402008 + (-4) - 0x4011d6-0x1c)

  =(unsigned)(0xe12)。

当执行lea指令时,PC值应为下一条指令地址,即0x4011f6,而0x4011f6+0xe12恰好等于0x402008,即.rodata段的地址。

图5-5-3 验证偏移量

5.6 hello的执行流程

使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。

ld-2.31.so!_dl_start

ld-2.31.so!_dl_init

hello!_start 

libc-2.31.so!__libc_start_main

libc-2.31.so!__cxa_atexit

libc-2.31.so!__libc_csu_init

hello!_init

libc-2.31.so!_setjmp

libc-2.31.so!_sigsetjmp

libc-2.31.so!__sigjmp_save

hello!main 

hello!puts@plt 

hello!exit@plt 

hello!printf@plt

hello!sleep@plt 

hello!getchar@plt

ld-2.31.so!_dl_runtime_resolve_xsave

ld-2.31.so!_dl_fixup

ld-2.31.so!_dl_lookup_symbol_x

libc-2.31.so!exit

5.7 Hello的动态链接分析

动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。虽然动态链接把链接过程推迟到了程序运行时,但是在形成可执行文件时(注意形成可执行文件和执行程序是两个概念),还是需要用到动态链接库。比如我们在形成可执行程序时,发现引用了一个外部的函数,此时会检查动态链接库,发现这个函数名是一个动态链接符号,此时可执行程序就不对这个符号进行重定位,而把这个过程留到装载时再进行。

PLT:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。

GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。

如图5-7-1所示,.got的地址为0x403ff0。

图5-7-1 .got的地址为0x403ff0

调用dl_init之前的.got和调用dl_init之后的.got,分别如图5-7-2和图5-7-3所示。从图中可以看出,在dl_init调用之后,该处的两个8字节的数据都发生了改变。

和PLT联合使用时,GOT[0]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。其中GOT[1]指向重定位表(依次为.plt节需要重定位的函数的运行时地址)用来确定调用的函数地址, GOT[2]是动态链接器ld-linux.so模块中的入口点。

 

图5-7-2 调用dl_init之前的.got

图5-7-3 调用dl_init之后的.got

5.8 本章小结

本章简要介绍了链接的概念与作用、在Ubuntu下链接的命令,然后分析了可执行目标文件hello的格式、hello的虚拟地址空间以及链接的重定位分析过程。最后,利用edb分析了hello的执行流程以及hello的动态链接分析。

此时,我们的hello程序已经成为了一个青年,具备了“社交”的能力,在链接器的帮助下,和外界的程序产生了联系,以逐步实现自己的价值。

(第51分)

6章 hello进程管理

6.1 进程的概念与作用

进程的概念:进程的经典定义就是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文(context)中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。

进程的作用:进程提供给应用程序的关键抽象:一个独立的逻辑控制流,好像我们的程序独占地使用处理器;一个私有的地址空间,好像我们的程序独占地使用内存系统。[2]

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

Shell的作用:shell代表用户运行其他程序。shell执行一系列的读/求值步骤,然后终止。读步骤读取来自用户的一个命令行。求值步骤解析命令行,并代表用户运行程序。

Shell的处理流程:

1读取从键盘输入的命令。

2判断命令是否正确,且将命令行的参数改造为系统调用execve()内部处理所要求的形式。

3终端进程调用fork()来创建子进程,自身则用系统调用wait()来等待子进程完成。

4 当子进程运行时,它调用execve()根据命令的名字指定的文件到目录中查找可行性文件,调入内存并执行这个命令。

5 如果命令行末尾有后台命令符号&终端进程不执行等待系统调用,而是立即发提示符,让用户输入下一条命令;如果命令末尾没有&则终端进程要一直等待。当子进程完成处理后,向父进程报告,此时终端进程被唤醒,做完必要的判别工作后,再发提示符,让用户输入新命令。

6.3 Hello的fork进程创建过程

父进程通过调用fork函数创建一个新的运行的子进程。新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时。子进程可以读写父进程中打开的任何文件。

例如,当我们输入./hello 7203610530 谭棣夫 1,shell通过对我们输入的命令进行解析后,发现我们输入的命令不是一个内置命令,然后shell就会将我们输入的第一个参数./hello看作一个可执行文件,并在相应对路径下寻找,所以shell在当前目录下找到了hello文件。然后shell就会调用fork()创建一个子进程,该子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本。

fork函数被调用一次,会返回两次:一次是在调用进程(父进程)中,一次是在新创建的子进程中。在父进程中,fork返回子进程的PID。在子进程中,fork返回0。

6.4 Hello的execve过程

execve函数在当前进程的上下文中加载并运行一个新程序。

execve 函数加载并运行可执行目标文件filename,且带参数列表 argv 和环境变量列表 envp。只有当出现错误时,例如找不到 filename,execve 才会返回到调用程序。

加载并运行 a.out 需要以下几个步骤:

删除已存在的用户区域。删除当前进程虛拟地址的用户部分中的已存在的区域结构。

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

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

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

图6-4-1 加载器是如何映射用户地址空间的区域的

6.5 Hello的进程执行

内核为每个进程维持一个上下文(context)。上下文就是内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表、包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。

一个进程执行它的控制流的一部分的每一时间段叫做时间片。在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策就叫做调度(scheduling),是由内核中称为调度器(scheduler)的代码处理的。当内核选择一个新的进程运行时,我们说内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程,上下文切换1)保存当前进程的上下文,2)恢复某个先前被抢占的进程被保存的上下文,3)将控制传递给这个新恢复的进程。

例如,当hello程序进行sleep系统调用时,它请求让调用进程休眠,这时,如果调用成功,则当前进程会休眠,从而进行一个上下文切换,切换到另一个进程。

为了使操作系统内核提供一个无懈可击的进程抽象,处理器通常是用某个控制寄存器中的一个模式位(mode bit),来限制一个应用可以执行的指令以及它可以访问的地址空间范围。当设置了模式位时,进程就运行在内核模式中(有时叫做超级用户模式)。一个运行在内核模式的进程可以执行指令集中的任何指令,并且可以访问系统中的任何内存位置。没有设置模式位时,进程就运行在用户模式中。用户模式中的进程不允许执行特权指令(privileged instruction)。也不允许用户模式中的进程直接引用地址空间中内核区内的代码和数据。

运行hello进程初始时是在用户模式中的。当异常发生时,控制传递到异常处理程序,处理器将模式从用户模式变为内核模式。处理程序运行在内核模式中,当它返回到应用程序代码时,处理器就把模式从内核模式改回到用户模式。

图6-5-1 进程的上下文切换

6.6 hello的异常与信号处理

6.6.1 异常

在hello执行的过程中,可能会出现四种异常:中断(interrupt)、陷阱(trap)、故障(fault)和终止(abort)。中断是来自处理器外部I/O设备的信号结果。例如,在hello中调用printf函数进行输出时会引起中断异常。陷阱是执行一条指令的结果。例如,在hello中,调用exit(1)终止当前进程需要向内核请求服务,则需要执行syscall指令,从而导致一个到异常处理程序的陷阱,这个处理程序解析参数,并调用适当的内核程序。故障是由错误情况引起。例如,在执行hello程序的时候,可能会发生缺页故障。终止时不可恢复的错误,在hello执行过程可能会出现DRAM或者SRAM位损坏的奇偶错误。

       6.6.2 信号及处理

可能产生的信号以及其默认的行为如图6-6-1所示。此外,用户可以自己编写信号处理程序,并且用signal函数修改和信号相关联的默认行为(除SIGSTOP和SIGKILL之外)。

 

图6-6-1 常见的Linux信号

6.6.3 hello的运行

  1. 随便乱按:运行结果如图6-6-2所示,乱按会将输入的内容存到缓冲区,作为接下来的命令行输入。

 

图6-6-2随便乱按后hello的运行结果

  1. Ctrl+c:如图6-6-3所示,按下Ctrl+c后,内核会发送一个SIGINT信号终止当前进程。

图6-6-3 Ctrl+c后hello的运行结果

  1. Ctrl+z:如图6-6-4所示,按下Ctrl+z后,shell父进程收到SIGSTP信号,hello进程停止了,并打印当前进程状态Stopped。

 

图6-6-4 Ctrl+z后hello的运行结果

  1. Ctrl+z后运行ps:如图6-6-5所示,Ctrl+z后运行ps,显示当前系统的进程状态。

 

图6-6-5 Ctrl+z后运行ps,hello的运行结果

  1. Ctrl+z后运行jobs:如图6-6-6所示,Ctrl+z后运行jobs,打印当前作业的信息。

 

图6-6-6 Ctrl+z后运行jobs,hello的运行结果

  1. Ctrl+z后运行pstree:如图6-6-7所示,Ctrl+z后运行pstree,所有进程以树状图显示。

 

图6-6-7 Ctrl+z后运行pstree,hello的运行结果

  1. Ctrl+z后运行fg:如图6-6-8所示,Ctrl+z后运行fg,后台运行的或挂起的任务(或作业)切换到前台继续运行。

 

图6-6-8 Ctrl+z后运行fg,hello的运行结果

  1. Ctrl+z后运行kill:如图6-6-9所示,Ctrl+z后运行kill,输入kill -9 4793,其中,4793是hello的PID,可以将hello进程终止。当我们再次用ps来获取系统进程的信息时,可以看到,他打印了[1]+  Killed                  ./hello 7203610530 谭棣夫 1,即hello进程已被终止。

 

图6-6-9 Ctrl+z后运行kill,hello的运行结果

6.7本章小结

本章主要介绍了hello进程管理,包括进程的概念和作用以及壳Shell-bash的作用与处理流程,还通过hello程序,介绍了hello的fork进程创建过程、hello的加载以及hello的进程执行。同时,还运行了hello程序,通过截图说明了hello执行的过程中,可能发生的各种异常、以及各种信号的处理。

在本章中,我们的hello程序通过进程,来处理遇到的各种异常、以及捕获的各种信号,以保证有条不紊地完成它的使命。

(第61分)

7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:在有地址变换功能的计算机中,访内指令给出的地址 (操作数) 叫逻辑地址,也叫相对地址。要经过寻址方式的计算或变换才得到内存储器中的实际有效地址,即物理地址。[4]例如,如图7-1-1所示,在hello.s中,以下三条指令均使用相对寻址的方式,即给出了段标识符以及偏移量。

图7-1-1 hello中的逻辑地址

线性地址:线性地址(Linear Address)是逻辑地址到物理地址转换的中间层。程序代码经编译后会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。若启用了分页机制,则线性地址会再此转换产生一个物理地址。若没有启用分页机制,则线性地址就是物理地址。

虚拟地址:现代处理器使用一种称为虚拟寻址(virtual addressing)的寻址形式。使用虚拟寻址,CPU通过生成一个虚拟地址(Virtual Address,VA)来访问主存,这个虚拟地址在被送到内存之前先转换成适当的物理地址。如图7-1-2所示,在对可执行目标文件hello进行反汇编之后的结果中,第一列就是每一条指令所占有的虚拟地址。并且,在Linux中,对于64 位地址空间,代码段总是从虚拟地址0x400000 开始。数据段跟在代码段之后,中间有一段符合要求的对齐空白。栈占据用户进程地址空间最高的部分,并向下生长。

 

图7-1-2 hello中的虚拟地址

物理地址:物理地址,也叫实地址、二进制地址,它是在地址总线上,以电子形式存在的,使得数据总线可以访问主存的某个特定存储单元的内存地址。当得到hello的虚拟地址后,可以通过MMU将虚拟地址翻译为物理地址,从而在内存中进行访问。

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

一个逻辑地址由两部分组成,段标识符,段内偏移量。段标识符是一个16位长的字段组成,称为段选择符,其中前13位是一个索引号。后面三位包含一些硬件细节。

索引号就是段描述符的索引。段描述符具体描述了一个段地址,这样,很多段描述符就组成段描述符表。可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符。

Base字段,表示的是包含段的首字节的线性地址,也就是一个段的开始位置的线性地址。

一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。

首先,给定一个完整的逻辑地址[段选择符:段内偏移地址],

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

拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。把Base + offset,就是要转换的线性地址了。

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

MMU利用页表来实现从虚拟地址空间(VAS)中的一个元素到物理地址空间(PAS)中的一个元素的映射。

如图7-3-1所示,CPU 中的一个控制寄存器,页表基址寄存器(Page Table Base Register, PTBR)指向当前页表。

n位的虚拟地址包含两个部分:一个p位的虛拟页面偏移(Virtual Page Offset,VPO)和一个(n-p)位的虚拟页号(Virtual Page Number, VPN)。

MMU利用VPN在作为到页表中的索引,来选择适当的 PTE。如果此时,有效位位0,那么就出现缺页。有效位为1时,才从页表条目中取出信息物理页号(PPN)。从而将页表条目中物理页号(Physical Page Number, PPN)和虚拟地址中的 VPO 串联起来,就得到相应的物理地址。注意,因为物理和虚拟页面都是P字节的,所以物理页面偏移 (Physical Page Offset, PPO)和VPO是相同的。

 

图7-3-1 使用页表的地址翻译

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

首先是TLB,其结构如图7-4-1所示。TLB也就是翻译后备缓冲器是一个包含在MMU中的小缓存,其每一行都由一个PTE组成。TLB将一个n-p位VPN分为t位的组索引和n-t-p位的标记。TLB是利用 VPN的位进行虚拟寻址的。因为TLB有4个组,所以 VPN的低2位就作为组素引(TLBI)。VPN 中剩下的高6位作为标记(TLBT),用来区别可能映射到同一个TLB组的不同的 VPN。

 

图7-4-1 TLB

进行翻译时,虚拟地址中的VPN被划分为VPN1,VPN2,VPN3,VPN4。CR3寄存器中有L1页表的地址,根据VPN1能够在L1页表找到相应PTE,得到L2页表的基地址,以此类推,最终我们得到物理地址。并进行之后访问。具体流程如下图7-4-2所示。

 

图7-4-2 TLB与四级页表支持下的VA到PA的变换流程

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

在从TLB或者页表中得到物理地址后,根据物理地址从cache中寻找。到了L1里面以后,寻找物理地址要检测是否命中,不命中则紧接着寻找下一级cache L2,接着L3,如果L3也不命中,则需要从内存中将对应的块取出放入cache中,其中可能会发生块的替换等其它操作。这里就是使用到CPU的高速缓存机制了,一级一级往下找,直到找到对应的内容。

7.6 hello进程fork时的内存映射

当fork函数被hello进程调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。

当fork在hello进程中返回时,hello进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。

7.7 hello进程execve时的内存映射

execve 函数在当前进程中加载并运行包含在可执行目标文件a.out中的程序,用a.out 程序有效地替代了当前程序。加载并运行 a.out 需要以下几个步骤:

删除已存在的用户区域。删除当前进程虛拟地址的用户部分中的已存在的区域结构。

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

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

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

 

图7-7-1 加载器是如何映射用户地址空间的区域的

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

如图7-8-1所示。

当MMU翻译某个地址时,触发了缺页异常。此时,控制便会转移到内核的缺页处理程序。处理程序会判断:

1.虚拟地址是否合法。判断虚拟地址是否处在区域结构定义的区域内。如果这个指令是不合法的,那么缺页处理程序就触发一个段错误,从而终止这个进程。

2.试图进行内存访问是否合法。判断进程是否有读、写或者执行这个区域内页面的权限。如果试图进行的访问是不合法的,那么缺页处理程序会触发一个保护异常,从而终止这个进程。

当内核知道缺页是正常情况时,会选择牺牲页,换入新的页面并进行页表更新,完成缺页处理。

 

图7-8-1 Linux缺页处理

7.9动态存储分配管理

动态内存分配器维护着一个进程的虚拟内存区域,称为堆。系统之间的细节不同,但不失通用性,假设堆是一个请求二进制零的区域,紧接在未初始化数据区域后开始,向上生长。对每个进程,内核维护一个全局变量brk指向堆顶。分配器将堆视为一组不同大小的块的集合来维护。

分配器的具体操作过程以及相应策略:

放置已分配块:当一个应用请求一个k字节的块时,分配器搜索空闲链表。查找一个足够大可以放置所请求的空闲块。执行这种搜索的常见策略包括首次适配、下一次适配和最佳适配等。

分割空闲块:一旦分配器找到了匹配的空闲块,需要决定分配这个空闲块中多少空间。可以选择用整个块,但会造成额外的内部碎片;也可以选择将空闲块分割为两部分,第一部分变成已分配块,剩下的变成新的空闲块。

获取额外的堆内存:如果分配器不能为请求块找到空闲块,分配器通过调用sbrk函数,向内核请求额外的堆内存。分配器将额外的内存转化成一个大的空闲块,将这个块插到空闲链表中,然后被请求的块放在这个新的空闲块中。

合并空闲块:分配器释放一个已分配块时,要合并相邻的空闲块。分配器决定何时执行合并,可以选择立即合并或者推迟合并。合并时需要合并当前块和前面以及后面的空闲块。

7.10本章小结

本章主要介绍了hello的存储管理。其中包括了hello的存储器地址空间,以及在不同环境下地址的翻译,包括Intel逻辑地址到线性地址到变换、Hello的线性地址到物理地址到变换、TLB与四级页表支持下的VA到PA的变换、三级Cache支持下的物理内存访问。同时,还介绍hello进程fork和execve时的内存映射和、缺页故障与缺页中断处理以及动态存储分配管理。

在本章中,我们的hello程序运用不同的硬件、软件条件,利用巧妙的方法,充分利用和管理庞大又有限的存储空间,为了让程序能够更加有条不紊地运行。

(第7 2分)

8章 hello的IO管理

8.1 Linux的IO设备管理方法

以下格式自行编排,编辑时删除

设备的模型化:文件

设备管理:unix io接口

8.2 简述Unix IO接口及其函数

以下格式自行编排,编辑时删除

8.3 printf的实现分析

以下格式自行编排,编辑时删除

[转]printf 函数实现的深入剖析 - Pianistx - 博客园

从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.

字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。

显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

以下格式自行编排,编辑时删除

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。

getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

以下格式自行编排,编辑时删除

(第81分)

结论

hello的一生从一个高级语言程序hello.c开始。

hello.c首先经过预处理器(cpp)得到修改了的源程序hello.i。预处理中会展开以#起始的行,试图解释为预处理指令。

接着,编译器(cc1)将hello.i翻译为汇编程序hello.s。生成的汇编语言程序(.s) 既比预处理文件(.i)更容易让机器理解、又比可重定位目标文件(.o)更容易让程序员理解。

然后,汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中。

接下来,链接器将各种代码和数据片段收集并组合成为一个可执行目标文件hello,这个文件可被加载(复制)到内存并执行。

当我们运行hello时,在shell中利用fork()函数创建子进程,为子进程分配一个与父进程相同但独立的虚拟内存空间,实行写时复制机制,再用execve()加载hello程序,这时,hello就由程序(program)变成了一个进程(process)。

CPU通过取指、译码、执行、访存、写回、更新PC等执行hello程序。

映射虚拟内存,程序开始时载入物理内存。通过Cache1、2、3和TLB多级页表,进行地址翻译。

IO管理和Shell的进程处理和信号处理机制可以处理hello运行过程中的信号。

当进程结束时,shell会回收hello进程,并且内核会从系统中删除hello所有痕迹。

至此,hello程序结束了它伟大的一生。而对于程序员来说,在计算机里,万万千千的程序也有着类似的经历。它们默默无闻地在计算机系统中,利用巧妙的机制运行,已经深深地改变了我们的世界。它们的一生,是所有计算机科学家智慧的结晶,也是需要我们去书写的未来。科技改变世界。

附件

hello.i:展示hello.c经过预处理的结果。

hello.s:展示hello.i经过编译的结果。

hello.o:展示hello.s经过汇编的结果。

hello:hello.o与其他可重定位目标文件链接生成的可执行文件

(附件0分,缺失 -1分)

参考文献

为完成本次大作业你翻阅的书籍与网站等

  1. 百度百科 https://baike.baidu.com/item/预处理/7833652?fr=aladdin
  2. RANDALE.BRYANT, DAVIDR.O‘HALLARON. 深入理解计算机系统[M]. 机械工业出版社, 2011.
  3. https://wiki.mbalib.com/wiki/机器语言
  4. https://baike.baidu.com/item/逻辑地址/3283849

(参考文献0分,缺失 -1分)

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值