HITICS大作业——程序人生

摘  要

为了深入理解一个程序从无到有,从运行到终止的过程,本文论述了hello.c在编写完成后运行在Linux系统中的生命历程,运用相关工具展示了hello.c文件预处理、编译、汇编、链接、运行、回收等阶段并进行分析。同时介绍了shell的内存管理,IO管理,进程管理等相关知识,了解虚拟内存、异常信号等内容,通过本次大作业实现对书本中知识的更深入理解。

关键词:计算机系统;编译;进程;虚拟内存                           

目  录

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

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

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

1.3 中间结果............................................................................... - 5 -

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

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

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

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

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

2.4 本章小结............................................................................. - 10 -

第3章 编译.................................................................................. - 11 -

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

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

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

3.4 本章小结............................................................................. - 19 -

第4章 汇编................................................................................. - 20 -

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

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

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

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

4.5 本章小结............................................................................. - 27 -

第5章 链接................................................................................. - 28 -

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

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

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

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

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

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

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

5.8 本章小结............................................................................. - 39 -

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

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

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

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

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

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

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

6.7本章小结.............................................................................. - 47 -

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

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

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

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

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

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

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

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

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

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

7.10本章小结............................................................................ - 56 -

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

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

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

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

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

8.5本章小结.............................................................................. - 62 -

结论............................................................................................... - 62 -

附件............................................................................................... - 64 -

参考文献....................................................................................... - 65 -

第1章 概述

1.1 Hello简介

Hello的P2P是指,hello程经过预处理,编译,汇编,链接得到可执行目标文件。

  1. 程序员在文本编辑器中编写程序的源代码,保存为hello.c文件。
  2. 预处理阶段:预处理器根据以#开头的命令来修改原始程序生成hello.i文本文件。
  3. 编译阶段:编译器将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序,该程序包含函数main的定义。
  4. 汇编阶段:汇编器将hello.s翻译成机器语言指令,打包成可重定位目标程序的格式,结果保存在hello.o。
  5. 链接阶段:链接器将目标文件与库文件链接在一起,生成可执行文件。

图 1 编译系统

Hello的020是指hello如何在进程中执行并被回收的过程。

通过shell输入./hello命令开始执行程序,shell通过fork函数创建它的子进程,再由子进程执行execve函数加载hello,在execve函数执行hello程序后,内核为其映射虚拟内存、分配物理内存,程序开始执行,内核为程序分配时间片执行逻辑控制流,当hello运行结束,shell接受到相应的信号,启动信号处理机制,对该进程进行回收处理,释放其所占的内存并删除有关进程上下文。

1.2 环境与工具

硬件环境:12th Gen Intel(R) Core(TM) i9-12900H   2.50 GHz  x64

软件环境:windows 11 64位;Ubuntu 22.04.03;VMware 16.2.2 build-19200509

使用工具:codeblocks;gcc;gdb;edb;objdump

1.3 中间结果

1)hello.c:源代码

(2)hello.i:预处理后的文本文件

(3)hello.s:编译后的汇编文件

(4)hello.o:汇编后的可重定位目标执行文件

(5)hello:链接之后的可执行文件

(6)hello_elf.txt:readelf读取hello.o得到的ELF格式信息

(7)hello1_elf.txt:用readelf读取hello得到的ELF格式信息

(8)hello.txt:hello反汇编的结果

(9)hello_o.txt:hello.o反汇编的结果

1.4 本章小结

     本章主要简单介绍了hello的P2P,020过程,并且简要介绍了P2P和020是什么,展示了一个源程序经过预处理、编译、汇编、链接等阶段,最终成为一个可执行目标文件的过程,最后列出了本次实验的实验环境和使用工具以及实验过程产生的中间文件。

第2章 预处理

2.1 预处理的概念与作用

预处理是编程中编译源代码的第一步,它在编译器开始工作之前执行。在C、C++等语言中,预处理器不是一个编译器的组成部分,而是一个独立的程序,它读取源代码文件,根据预处理指令(常见的有#include、#define、#ifndef、#endif等等)来处理源程序的文本,然后生成一个修改后的源文件,这个文件随后将被编译器处理。

预处理的作用:

  1. 宏替换:预处理器会处理源代码中的宏定义(通常用#define指令定义),它会将所有宏调用替换为它们的值或定义的代码块,通过使用宏名来代替一段字符串,方便程序员编写程序,有利于代码的简洁性和可读性。
  2. 文件包含:#include指令告诉预处理器将另一个文件的内容包含进来,可以用来共享代码,比如头文件,保证了之后的编译过程能够正确进行。
  3. 条件编译:预处理器支持条件编译,允许根据不同的条件包含或排除代码块,通常通过#ifdef、#ifndef、#endif等指令实现,可以有效删去一些无用代码,从而减少编译过程的工作量。

2.2在Ubuntu下预处理的命令

使用gcc -E hello.c -o hello.i指令进行预处理

图 2 gcc预处理指令

2.3 Hello的预处理结果解析

源程序hello.c

图 3 源程序

预处理后hello.i文本文件(部分截图)

图 4 预处理后文件

图 5 预处理后文件

预处理后,文件的格式仍为文本文件,文件的行数增加到了3092行,预处理后的文件中没有了#include等代码,说明预处理过程中将#include包含的文件加入到了源程序中。例如stdio.h是标准库文件,预处理器会到Linux系统的环境变量下寻找stdio.h,打开/usr/include/stdio.h。若stdio.h使用了“#define”“#include” 等,对它们进行递归展开替换,对于其中使用的“#ifdef”、“#ifndef”等条件编译语句,预处理器会对条件值进行判断来决定是否对此部分进行包含。

原来的hello.c文件中的注释都被删除,余下的原程序部分没有发生任何变化,在hello.i文件的结尾。

hello.i文件开头为源程序的相关信息。

图 6 hello.i文件开头信息

随后依次进行头文件stdio.h unistd.h stdlib.h的展开。

图 7 hello.i中头文件展开

类型定义信息

图 8 hello.i中类型定义信息

函数声明信息

图 9 hello.i中函数声明信息

hello.c源代码在hello.i文件结尾,除注释和以“#”开头的语句被删除外,其他内容保持不变。

图 10 hello.i中结尾信息

2.4 本章小结

本章介绍了预处理的相关概念、作用和linux系统中的预处理指令,并通过查看hello.i文件和hello.c文件对比他们的不同,分析了预处理的过程与结果。预处理过程实质上来说是文本增加、删除和替换的过程,是一个通过宏展开、宏替换、插入头文件等操作,使得程序中的宏引用被递归地替换掉的过程,生成.i文件后交给编译器进行处理。

第3章 编译

3.1 编译的概念与作用

C编译器在进行具体的程序翻译之前,会先对源程序进行词法分析和语法分析,然后根据分析的结果进行代码优化和存储分配,最终把C语言源程序翻译成汇编语言程序。总的说来,编译是指将预处理后的.i文件翻译成汇编语言程序.s文件的过程,这一过程由C编译器(ccl)完成。

编译的作用:

  1. 词法分析:编译器首先将源代码分解成一个个的词素,词素是编程语言中的基本元素,如关键字、标识符、操作符等。
  2. 语法分析:编译器根据编程语言的语法规则,将词素组织成语法树,确保源代码的结构符合语言规范。
  3. 语义分析:编译器检查语法树中的每个节点,确保它们在语义上是正确的。例如,它会检查变量是否已被声明、类型是否匹配等。
  4. 中间代码:源程序的一种内部表示,或称中间语言。中间代码的作用是可使编译程序的结构在逻辑上更为简单明确,特别是可使目标代码的优化比较容易实现中间代码。
  5. 优化:编译器对中间代码进行优化,以提高程序的执行效率。优化可以是局部的,也可以是全局的,目的是减少资源消耗和提高性能。
  6. 目标代码:目标代码生成器把语法分析后或优化后的中间代码变换成目标代码,此处指目标代码为汇编代码。可以说,编译的作用是通过一系列步骤让源代码更接近机器语言,编译是汇编阶段翻译成机器语言的前提。

3.2 在Ubuntu下编译的命令

使用gcc -m64 -no-pie -fno-PIC -S hello.i -o hello.s指令进行编译

图 11 gcc编译指令

图 12 编译后文件

3.3 Hello的编译结果解析

3.3.1数据

数据包括常量、变量(全局/局部/静态)、表达式、类型、宏,大作业程序中包含常量和局部变量,接下来对这两部分进行分析。

  1. 字符串常量

在hello.c中printf函数括号内的字符串"用法: Hello 学号 姓名 秒数!\n"以及"Hello %s %s\n"。

图 13 printf字符串

LC1里存放了"Hello %s %s\n",LC0存放了字符串"用法: Hello 学号 姓名 秒数!\n",汉字全部被替换成了“\+三个数字”的形式,这是因为在hello.s文件中汉字的表示是采用UTF-8编码,并用8进制表示,出来每个“\+三个数字”的结构表示一个字节。例如,“用”的UTF-8编码是E7 94 A8(16进制),将其16进制表示成8进制就成为了\347 \224 \250,英语字符的表示在hello.s中是正常表示的。两个字符串的信息都被放在了.rodata节中

图 14 hello.s中的字符串

  1. 整型数

在汇编语言中,$后加数字表示立即数。如c程序中argc!=5中的5就表示为$5。

图 15 hello.s中的整型数

  1. 局部变量i

局部变量在汇编语句中被放在寄存器里或栈里,本程序中有局部变量int i,i被存储在-4(%rbp)中,初始化为0,i占据了4字节的地址空间,关于局部变量i的进一步使用将在后面解析for循环时展示。

图 16 hello.s中的局部变量

  1. 参数argc

argc是用户传递给main函数的参数,被放在了堆栈中。

图 17 hello.s中的参数argc

  1. 数组argv[]

在hello.s中,其首地址保存在栈中,访问时通过寄存器寻址的方式访问。起始地址为%rbp-32,通过addq $8,%rax  addq $16,%rax  addq $24,%rax分别得到argv[1]和argv[2]和argv[3]。

图 18 hello.s中的数组argv

3.3.2赋值

赋值主要由mov指令实现,mov指令根据操作数的字节大小可以分为:movb:一个字节,movw:两个字节,movl:四个字节,movq:八个字节。

图 19 hello.s中的赋值

3.3.3算数运算

++操作,如将i++操作表示为

图 20 hello.s中的算数操作

3.3.4控制转移

(1)if语句

程序里开头使用了一个条件判断if(argc!=5),它被编译器翻译为cmp加上条件转移指令的形式。

图 21 源程序中的if语句

图 22 if语句的汇编实现

cmpl语句将edi寄存器的值和立即数5比较(实际上就是计算%edi-4),如果相等(计算结果为0),就将ZF标志设为1;如果不相等(计算结果不为0),就将ZF标志设为0。根据ZF的值进行条件跳转,从而实现了if语句的条件分支。根据je .L2语句,当ZF=1时,即argc等于5的时候,就会执行.L2标签内的代码;当ZF=0时,即argc不等于5的时候,程序就往下顺序执行。

(2)for循环语句

图 23 源程序中的for循环语句

图 24 for循环语句的汇编实现

首先为i赋初值为0。for循环的判断条件(i<10),当i<10的时候程序会执行for循环的循环体,通过cmpl语句比较寄存器ebp的值和常量9,此时cmpl会根据%ebp-7的结果修改多个标志的值,如果ebp的值小于9时,cmpl会将OF标志的值设为1,将SF标志的值设为1;如果ebp的值等于9,cmpl会将ZF标志的值设为1,jle条件跳转的跳转条件是(SF^OF|ZF),因此当ebp的值小于等于7的时候,jle的跳转条件成立,从而会执行for循环的循环体。

每次for循环后执行i++操作(上面介绍算数运算时提到),继续执行.L3标签内的代码,进行比较,若i小于等于9操作同上,若大于9则跳出循环执行后续操作。

3.3.5main函数

main函数被系统函数调用执行。

参数传递:main函数有两个参数int argc和char *argv[],两个参数分别通过寄存器edi和rsi传递表示。

图 25 main函数参数传递

参数char*argv[]表示的是一个字符指针数组,在参数-m64下进行编译,因此每一个指针的大小应该为8个字节,数组的访问在数据部分已经介绍。

返回值: main函数的返回值通过代码return 0实现,在hello.s中是通过设置eax寄存器实现。通过movl语句,将eax寄存器的值设置为0,从而设置了main函数的返回值。

图 26 hello.s中main函数返回值实现

3.3.6普通函数

参数传递:64位栈结构中是通过寄存器和内存共同实现的,第1~6个参数存放在寄存器%rdi,%rsi,%rdx,%rcx,%r8,%r9中,寄存器不够用时把多的参数存放在栈中。

函数调用:call指令会将返回地址压入栈中,并且将%rip的值设置为指向所调用函数的地址(等函数执行完之后调用ret弹出原来的%rip并且将栈帧结构恢复)。

函数的返回值通过寄存器eax保存。

  1. printf函数

图 27 调用printf函数

4个参数,把第一个参数字符串地址放在rdi中,第二个参数argv[1]放在rsi中,第三个参数argv[2]放在rdx中,第四个参数argv[4]放在rcx中,然后call printf,从printf返回。

  1. exit函数

图 28 调用exit函数

将1传给%edi,完成参数传递,call exit进行函数调用,从exit返回。

  1. sleep函数

图 29 调用sleep函数

编译器关于函数嵌套的处理与一般的处理是类似的,先处理内层函数,再将内层函数的返回值作为外层函数的参数,最后再处理外层函数。

将atoi的返回值%eax通过%rdi传递给sleep函数,call sleep调用sleep函数,从sleep中返回。

  1. atoi函数

将argv[4]通过%rdi传递给atoi函数,call atoi进行函数调用,从atoi中返回。

  1. getchar函数

图 30 调用getchar函数

无参数,调用后从getchar返回。

3.4 本章小结

本章介绍了编译的概念与作用,编译是将文本文件翻译成汇编语言程序,为后续将其转化为二进制机器码做准备的过程,由之前的C语言程序,转变成了汇编语言程序。以hello.c源程序为例进行编译,对结果进行解析,解析了汇编代码如何实现数据、赋值、算术操作、控制转移、函数调用等,加深对编译以及汇编语言的理解。

第4章 汇编

4.1 汇编的概念与作用

汇编的概念:汇编是将编译后产生的.s汇编文件翻译成机器语言指令的过程。这一过程由汇编器(as)完成。

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

汇编的作用:汇编的作用是将汇编语言代码转换成机器语言代码,并将结果保存在可重定位目标文件(.o二进制文件)中。汇编过程从汇编程序得到一个可重定位目标文件,以便后续进行链接。

4.2 在Ubuntu下汇编的命令

使用gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o指令进行汇编

图 31 gcc汇编指令

4.3 可重定位目标elf格式

ELF可重定位目标文件的典型格式

图 32 ELF可重定位文件典型格式

使用readelf -a hello.o > hello_elf.txt指令查看hello.o的ELF格式并保存到hello_elf.txt文件中。

图 33 查看hello.o的ELF格式文件

图 34 hello.o的ELF格式文件

4.3.1ELF头

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

图 35 ELF头信息

4.3.2节头部表

在节头部表里描述了不同节的名称、类型、地址、偏移量和大小等信息。

图 36 节头部表

4.3.3重定位节

当汇编器生成一个目标模块时,它并不知道数据和代码最终将放在内存中的什么位置,它也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。所以,无论何时汇编器遇到对最终位置未知的目标引用,它就会生成一个重定位条目,告诉链接器在将目标文件合并成可执行文件时如何修改这个引用。

重定位节记录了各段引用的符号相关信息,在链接时,需要通过重定位节对这些位置的地址进行重定位。链接器会通过重定位条目的类型判断如何计算地址值并使用偏移量等信息计算出正确的地址。

图 37 重定位节

4.3.4symtab节

符号表(.symtab)存放着程序中定义和引用的函数和全局变量的信息。

图 38 symtab节

4.4 Hello.o的结果解析

objdump -d -r hello.o  分析hello.o的反汇编,并与第3章的 hello.s进行对照分析。

图 39 hello.o反汇编

(1)数的表示:hello.s中的操作数是十进制,hello.o反汇编代码中的操作数是十六进制。

图 40 hello.s中操作数

图 41 hello.o反汇编中的操作数

(2)函数调用:在hello.s中调用函数,是call +函数名,而在hello.o反汇编中调用函数是通过PC相对寻址的方式,定位函数的地址。因为函数只有在链接之后才能确定运行执行的地址,因此在.rela.text节中为其添加了重定位条目。

图 42 hello.s中的函数调用

图 43 hello.o反汇编中的函数调用

(3)汇编中mov、push、sub等指令都有表示操作数大小的后缀(b : 1字节、w :2 字节、l :4 字节、q :8字节),反汇编得到的代码中则没有。hello.s中提供给汇编器的辅助信息在反汇编代码中不再出现,如“.cfi_def_cfa_offset 16”等。

图 44 hello.s中的指令

图 45 hello.o反汇编中的指令

(4)在hello.s中进行跳转,是跳转到某一个标签所关联的代码,使用格式串的时候,也是使用标签。而在hello.o的反汇编中,不再使用标签,而是用地址来代替(只不过由于需要重定位,因此hello.o的反汇编中跳转指令之后是相对偏移的地址,即间接地址)。

图 46 hello.s中的跳转

图 47 hello.o反汇编中的跳转

(5)反汇编代码除了汇编代码之外,还显示了机器代码,在左侧用16进制表示。

机器语言是用二进制代码表示的计算机能直接识别和执行的一种机器指令的集合。一条机器语言指令由操作码+操作数构成。操作码规定了指令的操作,是指令中的关键字,不能缺省。操作数表示该指令的操作对象。

机器语言与汇编语言的映射关系:汇编语言通过助记符来表示机器指令,并通过汇编器转换成机器语言。对于汇编语言的每一个指令如movq、leaq、popq等在机器语言中都有操作码与之对应,而且对于操作的寄存器不同,操作码也会有不同。

在汇编语言中,操作数可以是立即数、寄存器、内存地址或常量。然而,在机器语言中,所有的操作数都必须通过地址来引用。

在汇编语言中,可以使用标签或条件语句(如JMP, JE, JNE)来实现分支转移,汇编器将这些转换为包含跳转地址的机器指令。

在汇编语言中,函数调用通常通过调用指令(如CALL)和返回地址来实现。汇编器需要处理这些指令,生成包含目标函数地址的机器指令,并在栈上保存返回地址。

4.5 本章小结

本章介绍了汇编的概念与作用, hello.s文件汇编为hello.o文件,并生成hello.o的ELF格式文件写入hello_elf.txt,查看并总结了hello.o的elf文件格式的信息。同时使用反汇编查看hello.o经过反汇编过程生成的代码并与hello.s相比较,发现机器语言与汇编语言之间的映射关系和差别。

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指令进行链接

图 48 ld链接指令

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

使用readelf -a hello > hello1_elf.txt指令查看hello的ELF格式并保存到hello1_elf.txt文件中。

图 49 查看hello的ELF格式

5.3.1ELF头

以描述了生成该文件的系统的字的大小和字节顺序的16字节序列Magic开始,剩下的部分包含帮助链接器语法分析和解释目标文件的信息,如程序入口点的地址,程序头起点等。在可重定位目标文件的elf中,入口点地址和程序头起点是0,当程序被链接后生成的可执行文件中的elf中都被填入了正确的地址。

图 50 ELF头

5.3.2节头

在节头里描述了节的名称、类型、地址、偏移量、大小等信息。

图 51 节头部表

5.3.3程序头

程序头里描述了不同段的类型,段在文件中的偏移,段在虚拟内存中的地址,段在物理内存里的地址,段在文件中的大小,段在内存中的大小,段的属性(可读、可写、可执行)以及对齐等信息,描述了可执行文件中的节与虚拟空间中的存储段之间的映射关系

图 52 程序头

5.3.4重定位节

图 53 重定位节

5.3.5符号表

符号表中增加了许多内容。

图 54 symtab表

5.3.6Dynamic section

与可重定位目标文件相比,可执行文件增加了一个Dynamic section。

图 55 Dynamic section

5.4 hello的虚拟地址空间

使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。

虚拟地址空间的起始地址为0x400000。

图 56 虚拟地址空间起始地址

使用edb查看Loaded Symbols

图 57 Loaded Symbols

据节头部表,我们可以知道.rodata节开始于虚拟地址0x402000处,因此在edb中通过查看地址0x402000的内容可以找到位于.rodata节的格式串。

图 58 虚拟内存中的.rodata节

.inerp段的起始地址为04002e0。

图 59 虚拟内存中的.inerp段

.text段的起始地址为0x4010f0,0x4010f0处是程序的起始位置。

图 60 虚拟内存中的.text段

5.5 链接的重定位过程分析

通过objdump -d -r hello 对hello文件进行反汇编,结果如下:

图 61 hello的反汇编

在hello.o中跳转指令和call指令后为相对地址,而在hello中已经是重定位之后的虚拟地址。

图 62 hello.o中的相对地址

图 63 hello反汇编中的虚拟地址

hello反汇编中还多了一些节如.plt节等、多了外部函数的PLT信息、以及_start入口的代码。

图 64 .plt节内容

图 65 外部函数的PLT信息

图 66 _start入口

链接的过程:链接的过程主要包括符号解析和重定位两个步骤。在符号解析的过程中,链接器将每一个符号引用与它输入的可重定位目标文件中的符号表中的一个确定的符号定义关联起来。一旦链接器完成了符号解析这一步,就会开始重定位步骤。在重定位步骤里,链接器将所有相同类型的节合并为同一类型的新的聚合节,然后链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每一个节,以及赋给输入模块定义的每个符号。之后,链接器依赖重定位条目,针对重定位条目中的每个符号,修改代码节和数据节对这些符号的引用,使它们指向正确的运行时的地址。

重定位:一开始,链接器会进行节的合并以及将运行时内存地址赋给聚合节,赋给输入模块定义的每一个节,以及赋给输入模块定义的每个符号。完成了这一步的时候,代码中引用的每个符号都有了唯一的运行时的内存地址。根据重定位条目中的内容,修改代码节和数据节中对每个符号的引用。由于在重定位条目中有描述符号的类型(是PC相对引用或是绝对引用),也描述了重定位符号的偏移量(引用重定位符号的相对于.text节或者.data节的位置)以及一个特别的加数。用offset表示重定位符号的偏移量,用addend表示这个特别的加数。

如果符号是PC相对引用的,由于已经知道每一个节在运行时的内存地址(ADDRs)。那么引用这个重定位符号的位置refaddr,可以通过refaddr=offset+ADDRs计算得到,同时由于该重定位符号运行时内存地址已知(设为ADDRb),那么修改后的地址应该为ADDRb-refaddr+addend。如果符号是绝对引用,那么修改后的地址应该就是这个重定位符号运行时的内存地址。

由下图中可以知道exit函数运行时在内存中的地址为0x4010d0。在第四章的重定位条目中知道exit的偏移量为0x29,加数为-4(因为32位地址,占了4个字节)。由第五章之前的节头部表知.text节运行时在内存的地址为0x4010f0,从而可以计算得需要修改对exit的引用的位置为0x4010f0+0x29=0x401119,从而可以计算得修改后的结果为0x4010d0-0x401119+(-0x4)=0xffffff7e,从而重定位后修改的结果为0xffffff7e,在hello的反汇编中是小端表示,因此操作码后面应该是7e ff ff ff。

图 67 exit运行时的内存地址

图 68 .text节运行时的内存地址

图 69 hello中调用exit

5.6 hello的执行流程

  1. 调用了动态链接库linux-x86-64.so.2、libc.so中的几个函数
  2. _start
  3. __libc_start_main
  4. __cxa_atexit
  5. libc.so中几个函数
  6. 动态链接库libc.so.6里的函数
  7. hello!main
  8. hello!puts@plt
  9. hello!exit@plt
  10. hello! printf@plt
  11. hello!sleep@plt
  12. hello!getchar@plt

5.7 Hello的动态链接分析

节头部表中有如下信息:

在dl_init前,.got节.got.plt节的内容如下:

在dl_init后,.got节.got.plt节的内容如下:

.got节、.got.plt节在内存里的内容都发生了改变。

在dl_init调用之前,对于每一条PIC函数调用,调用的目标地址都实际指向PLT中的代码,GOT存放的是PLT中函数调用指令的下一条指令地址。在函数调用时,首先跳转到PLT执行.plt中操作,第一次访问跳转时GOT地址为下一条指令,将函数序号入栈,然后跳转到PLT[0],之后将重定位表地址入栈,访问动态链接器,在动态链接器中使用在栈里保存的函数序号和重定位表计算函数运行时的地址,重写GOT,返回调用函数。之后如果还有对该函数的访问,就不用执行第二次跳转,直接参看GOT信息。

5.8 本章小结

本章对hello.o进行链接,获得可执行文件hello,查看了hello的elf格式并对其进行了分析,查看了hello的虚拟空间地址的各段信息,利用objdump查看hello.o与hello的不同,分析说明了在链接过程中的重定位过程,查看了hello的执行流程,经历的函数,最后对hello的动态链接进行了简要的分析。

6章 hello进程管理

6.1 进程的概念与作用

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

在现代系统上运行一个程序时,我们会得到一个假象,就好像我们的程序是系统中当前运行的唯一的程序一样,我们的程序好像是独占地使用处理器和内存,处理器就好像是无间断地一条接一条地执行我们程序中的擅令,我们程序中的代码和数据好像是系统内存中唯一的对象,这些假象都是通过进程的概念提供给我们的。

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

shell是一个交互型应用级程序,代表用户运行其他程序。它通过执行一系列的读/求值步骤,读取用户的命令行,解析命令,然后代表用户运行程序。Shell的功能主要有负责各进程创建与程序加载运行及前后台控制,作业调用,信号发送与管理等。

处理流程:

(1)终端进程读取用户由键盘输入的命令行。

(2)分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量。

(3)检查第一个命令行参数是否是一个内置的shell命令。

(3)如果不是内部命令,调用fork( )创建新进程/子进程。

(4)在子进程中,用步骤2获取的参数,调用execve( )执行指定程序。

(5)如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid(或wait...)等待作业终止后返回。

(6)如果用户要求后台运行(如果命令末尾有&号),则shell返回。

6.3 Hello的fork进程创建过程

输入命令执行 hello 后,父进程如果判断不是内部指令,即会通过 fork 函数创建子进程。子进程与父进程相似,并得到一份与父进程用户级虚拟空间相同且独立的副本(包括数据段、代码、共享库、堆和用户栈)。父进程打开的文件子进程也可读写,二者之间的PID的不同。fork函数只会被调用一次,但会返回两次,在父进程中,fork返回子进程的PID,在子进程中,fork返回0。

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

6.4 Hello的execve过程

当fork之后,子进程调用execve函数(传入命令行参数)在当前进程的上下文中加载并运行一个新程序即hello程序,execve调用驻留在内存中的被称为启动加载器的操作系统代码来执行hello程序,加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零,通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容。最后加载器设置PC指向_start地址,_start最终调用hello中的main函数。除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制。直到CPU引用一个被映射的虚拟页时才会进行复制,这时,操作系统利用它的页面调度机制自动将页面从磁盘传送到内存。需要注意的是,execve成功调用的时候不会返回到调用程序,只有出现错误了,才会返回到调用程序。

6.5 Hello的进程执行

上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。

逻辑控制流:一系列程序计数器PC的值的序列叫做逻辑控制流,进程是轮流使用处理器的,在同一个处理器核心中,每个进程执行它的流的一部分后被抢占(暂时挂起),然后轮到其他进程。

时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。为了实现多个进程轮流运行,操作系统会给进程分配时间片。当操作系统调度hello进程的时候,系统会给hello进程分配时间片,在这一时间片里,hello进程可以独占地使用处理器。

用户模式和内核模式:处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。而一个进程从用户模式变为内核模式的唯一方法是通过诸如中断、故障或者陷阱这样的异常。上下文切换一定发生在内核模式下。

hello sleep进程调度的过程:调用sleep之前,若hello程序不被抢占则顺序执行,若发生被抢占的情况,则进行上下文切换,并进行如下操作:

(1)保存以前进程的上下文

(2)恢复新恢复进程被保存的上下文

(3)将控制传递给这个新恢复的进程 ,来完成上下文切换。

hello初始运行在用户模式,在hello进程调用sleep之后陷入内核模式,内核处理休眠请求主动释放当前进程,并将hello进程从运行队列中移出加入等待队列,定时器开始计时,内核进行上下文切换将当前进程的控制权交给其他进程,当定时器到时时发送一个中断信号,此时进入内核状态执行中断处理,将hello进程从等待队列中移出重新加入到运行队列,成为就绪状态,hello进程就可以继续进行自己的控制逻辑流了。

图 70 进程上下文切换的剖析

6.6 hello的异常与信号处理

6.6.1正常执行

图 71 正常执行状态

6.6.2乱按键盘

在按键盘的时候会发生中断异常,hello进程会进入内核模式,将控制转移给中断异常处理程序。键盘的中断处理程序,会从键盘控制器的寄存器读取扫描码并翻译成ASCII码,并存入键盘缓冲区。在按了回车键的时候,输入的字符串会被shell识别为命令。

图 72 乱按键盘后的执行结果

6.6.3Ctrl-Z

进程收到SIGTSTP信号,信号的动作是将hello挂起,用ps命令可以查看当前的所有进程的进程号,用jobs命令看到job ID是1,状态是“已停止”。

图 73 ctrl+z

输入pstree命令:以树状图显示进程间的关系。

图 74 pstree

用fg命令可以将指定的作业放在前台运行,此时会给指定的进程组发送SIGCONT信号,让挂起的进程重新运行。用kill命令可以向指定的进程组发送信号,kill -9表示发送SIGINT信号,会让进程组内每一个进程终止。

图 75 fg kill

6.6.4Ctrl-C

在hello程序运行时输入CTRL+C会导致内核发送一个SIGINT信号到前台进程组的每个进程,默认情况下,结果是终止前台作业。

图 76 ctrl+c

6.6.5总结

1. 中断:异步发生,是来自处理器外部的I/O设备的信号的结果。

处理:在当前指令的执行过程中,中断引脚电压变高,在当前指令完成后,控制传递给处理程序,中断处理程序运行,处理程序返回下一条指令。

2. 陷阱:同步发生,有意的异常,是执行一条指令的结果。

处理:应用程序执行一次系统调用,控制传递给处理程序,陷阱处理程序运行,处理程序返回到syscall之后的指令。

3. 故障:同步发生,由错误情况引起的,可能能够被故障处理程序修正。

处理:当前指令导致一个故障,控制传递给处理程序,故障处理程序运行

处理程序要么重新执行当前指令,要么终止。

4. 终止:同步发生,不可恢复的致命错误造成的结果,不将控制返回给应用程序。

处理:发生致命的硬件错误,控制传递给处理程序,终止处理程序运行,处理程序返回到abort例程。

6.7本章小结

本章主要介绍了hello在shell中是如何运行的,分析了hello执行过程中的进程管理。通过进程的概念和shell的工作流程,分析shell是如何通过调用fork函数为hello创建子进程,execve函数加载hello函数的,利用时间片的概念,分析的内核的进程调度过程,用户态和内核态的转换,最后分析了hello执行过程中遇到的异常和信号。

7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:是指由程序产生的与段相关的偏移地址部分,是程序代码经过编译后出现在汇编程序中地址。一个逻辑地址由一个段和偏移量组成。

线性地址:逻辑地址经过段机制后转化为线性地址(虚拟地址),是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。如果启用了分页机制,那么线性地址可以再经过变换以产生一个物理地址;如果没有启用分页机制,那么线性地址直接就是物理地址。

虚拟地址:虚拟地址空间和线性地址空间是相同的。

物理地址:是指出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。对于系统中物理内存的M个字节,都有{0,1,2,...,M-1}中的一个数与之一一对应,这个数就是该字节的物理地址。

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

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

一个逻辑地址由两部分组成:段标识符:段内偏移量。段标识符是一个16位长的字段(段选择符),可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。

索引号就是“段描述符”的索引,段描述符具体地址描述了一个段。很多个段描述符,就组成了一个数组,叫“段描述符表”,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,每一个段描述符由8个字节组成。全局的段描述符,放在“全局段描述符表(GDT)”中,一些局部的段描述符,放在“局部段描述符表(LDT)”中。

在保护模式下,段寄存器中存放着段选择符。16位段选择符(由13位索引,1位TI,2位RPL组成)。TI位表示索引的描述符表类别(TI=0,选择全局描述符表(GDT),TI=1,选择局部描述符表(LDT)),高13位索引可以用来确定当前使用的段描述符在描述符表中的位置,RPL表示特权级别。

在实际转换的时候,通过TI位决定去访问GDT还是LDT,然后根据13位索引去查找选定段的段描述符,通过段描述符中的内容可以得到段基址,将段基址与EA(偏移量)相加就得到了线性地址。

图 77 逻辑地址向线性地址转换

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

将程序的逻辑地址空间划分为固定大小的页,而物理内存划分为同样大小的页框。程序加载时可将任意一页放入内存中任意一个页框,这些页框不必连续,从而实现了离散分配。在页式存储管理方式中地址结构由两部分构成,前一部分是虚拟页号(VPN),后一部分是虚拟页偏移量(VPO)。

页表:页表将虚拟内存映射到物理地址空间,每次地址翻译硬件将一个虚拟地址转换为一个物理地址时,都会读取页表。页表是一个页表条目(PTE)的数组。虚拟地址空间中每个页在页表中一个固定偏移量处都有一个PTE,假设每个PTE是由是由一个有效位和n位地址字段组成的。有效位表明了该虚拟页当前是否被缓存在DRAM中,如果设置了有效位,那么地址字段就表示DRAM中相应的物理页的起始位置,这个物理页缓存了这个虚拟页;如果没有设置有效位,那么一个空地址表示这个页还没有被分配。

CPU里面有一个控制寄存器PTBR(页表基址寄存器),指向当前页表。通过PTBR找到页表的首地址,再根据VPN的值可以得到对应的页表条目的地址(PTEA)。PTEA=%PTBR+VPN*页表条目大小。

找到了页表条目后,如果有效位=1,说明该虚拟页缓存进了内存,从而根据PTE可以找到该虚拟页对应的物理页号。由于虚拟页和物理页大小相等,物理页中的页内偏移PPO=VPO。从而物理地址由PPN与VPO组合而成。

图 78 使用页表的地址翻译

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

  1. 快表

CPU产生一个虚拟地址(VPN和VPO的组合)

图 79 虚拟地址中用以访问TLB的组成部分

根据TLB索引(TLBI)去选择快表(TLB)中对应的组号,再根据TLB标记去匹配相应的路,如果匹配成功了,那么可以得到页表条目PTE,之后将PTE中的PPN与VPO组合,从而可以得到物理地址(PA)。如果匹配失败,快表中没有缓存这一页表条目,那么需要按照7.3中的步骤,从页表基址寄存器中得到页表的首地址,然后根据VPN的值,可以得到页表条目的地址PTEA=%PTBR+VPN*页表条目大小。根据PTEA的值,在高速缓存或者内存中取出PTE的值,并将新取出的PTE存放在快表中,得到了PTE后,就可以按照同样的方法得到PA。

图 80 TLB命中和不命中的操作图

  1. 四级页表

CPU产生一个虚拟地址,虚拟地址由VPN1,VPN2,VPN3,VPN4和VPO组合而成。

由于PTBR页表基址寄存器存放了一级页表的首地址,因此通过VPN1可以查看一级页表中,存放的相应的二级页表的地址,再通过VPN2查看二级页表中,存放的相应的三级页表的地址,再通过VPN3查看三级页表中,存放的相应的四级页表的地址,最后通过VPN4得到四级页表中相应的页表条目(PTE)。根据PTE可以得到物理页号PPN,通过将PPN与VPO组合从而得到了物理地址PA。

图 81 使用k级页表的地址翻译

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

高速缓存存储器(Cache)组织结构:

根据PA、L1高速缓存的组数和块大小确定高速缓存块偏移(CO)、组索引(CI)和高速缓存标记(CT),使用CI进行组索引,对组中每行的标记与CT进行匹配。如果匹配成功且块的valid标志位为1,则命中,然后根据CO取出数据并返回数据给CPU。

若未找到相匹配的行或有效位为0,则L1未命中,继续在下一级高速缓存(L2)中进行类似过程的查找。若仍未命中,还要在L3高速缓存中进行查找。三级Cache均未命中则需访问主存获取数据。若进行了上述操作,说明至少有一级高速缓存未命中,则需在得到数据后更新未命中的Cache。首先判断其中是否有空闲块,若有空闲块(有效位为0),则直接将数据写入;若不存在,则需根据替换策略(如LRU、LFU策略等)驱逐一个块再写入。

7.6 hello进程fork时的内存映射

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

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

7.7 hello进程execve时的内存映射

execve函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。

加载并运行hello需要以下几个步骤:

  1. 删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构。
  2. 映射私有区域,为新程序的代码、数据、bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区,bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零。
  3. 映射共享区域, hello程序与共享对象libc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。
  4. 设置程序计数器(PC),execve做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。

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

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

若程序想要访问某个虚拟页中的数据的时候,会产生一个虚拟地址。当MMU(内存管理单元)在试图翻译这个虚拟地址的时候,会发现该地址所在的虚拟页没有缓存进内存(即PTE中有效位为0),必须从磁盘中取出,这时候就会发生缺页故障。

缺页中断处理:

  1. 判断虚拟地址是否合法。缺页处理程序搜索区域结构的链表,把虚拟地址和每个区域结构中的vm_start和vm_end做比较。如果指令不合法,缺页处理程序会触发一个段错误,从而终止这个进程。
  2. 判断内存访问是否合法。比如缺页是否由一条试图对只读页面进行写操作的指令造成的。如果访问不合法,缺页处理程序会触发一个保护异常,从而终止这个进程。
  3. 内核知道缺页是由合法的操作造成的。内核会选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。处理程序返回时,CPU重新执行引起缺页的指令,这条指令将再次发送给MMU。这次,MMU能正常地进行地址翻译,不会再产生缺页中断。

图 83 Linux缺页处理

图 84 缺页操作图

7.9动态存储分配管理

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

分配器分为两种基本风格:显式分配器、隐式分配器。

1.显式分配器:要求应用显式地释放任何已分配的块。

2. 隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。

分配器有以下几种实现方式:

  1. 用隐式空闲链表的方式来组织空闲块。堆中的空闲块通过头部中的大小字段隐含地连接,分配器通过遍历堆中所有的块,从而间接遍历整个空闲块的集合。

分配器的放置策略有:首次适配(从头搜索,遇到第一个合适的块就停止)、下一次适配(从链表中上一次查询结束的地方开始,遇到下一个合适的块停止)和最佳适配(全部搜索,选择合适的块停止)等。

分割策略:不分割,使用整个空闲块;或者将空闲块分成两部分,第一部分变成分配块,剩下部分变成空闲块,获取额外的堆内存得到合适的空闲块。

合并空闲块的策略:立即合并(每次释放一个块的时候就合并)、推迟合并、带边界标记的合并。

  1. 用显式空闲链表的方式来组织空闲块。显式空闲链表结构将堆组织成一个双向空闲链表,在每个空闲块的主体中,都包含一个pred(前驱)和succ(后继)指针。使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。

维护链表的顺序:

  1. 后进先出的顺序,将新释放的块放置在链表的开始处。
  2. 按照地址顺序来维护链表,其中链表中每个块的地址都小于它后继的地址。
  1. 分离的空闲链表:维护多个空闲链表,其中,每个链表的块具有相同的大小。将所有可能的块大小分成一些等价类,从而进行分离存储。有两种基本方法:简单分离存储,分离适配。

7.10本章小结

本章主要介绍了hello的存储器地址空间、intel的段式管理、页式管理,TLB与四级页表支持下的VA到PA的变换、三级cache支持下物理内存访问, hello进程fork时的内存映射、execve时的内存映射、缺页故障与缺页中断处理、动态存储分配管理等内容。

8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

所有IO设备都被模型化为文件,所有的输入和输出都能被当做相应文件的读和写来执行。

设备管理:unix io接口

Linux内核有一个简单、低级的接口,称为Unix I/O,所有的输入和输出都能以一种统一且一致的方式来执行。

8.2 简述Unix IO接口及其函数

将设备优雅地映射成文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。

  1. 打开文件:一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
  2. Linux shell创建的每个进程开始时都有三个打开的文件:标准输入,标准输出和标准错误。
  3. 改变当前的文件位置。
  4. 读写文件:从文件复制n个字节到内存,从当前文件位置k开始,然后将k增加到k+n;写操作:从内存复制n个字节到文件,当前文件位置为k,然后更新k。
  5. 关闭文件:当应用完成了对文件的访问之后,它就通知内核关闭这个文件,作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。

8.2.1打开文件

进程通过调用open函数来打开一个已存在的文件或者创建一个新文件。

图 85 open函数

open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。

8.2.2关闭文件

进程通过调用close函数关闭一个打开的文件。

图 86 close函数

fd是需要关闭的文件的描述符,关闭一个已关闭的描述符会出错。

8.2.3读写文件

应用程序是通过分别调用read和write函数来执行输入和输出的。

图 87 read和write函数

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

write函数从内存buf复制至多n个字节到描述符fd的当前文件位置。若成功则返回值为写的字节数,若出错则为-1。

8.3 printf的实现分析

Printf函数如下

图 88 print函数

参数采用了可变参数的定义, *fmt是一个char 类型的指针,指向字符串的起始位置。这个指针指向第一个const参数(const char *fmt)中的第一个元素。fmt也是个变量,它的位置,是在栈上分配的,它也有地址。

printf调用的外部函数vsprintf如下:

图 89 vsprintf函数

vsprintf的作用就是格式化,它接受确定输出格式的格式字符串fmt,用格式字符串对个数变化的参数进行格式化,产生格式化输出,写入buf供系统调用write输出时使用。

write系统函数如下:

图 90 write系统函数

在write函数中,将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址,int INT_VECTOR_SYS_CALLA代表通过系统调用syscall。

sys_call实现如下:

图 91 sys_call实现

ecx中是要打印出的元素个数 ,ebx中的是要打印的buf字符数组中的第一个元素,这个函数的功能就是不断的打印出字符,直到遇到:’\0’ 停止。

总结:函数printf的实现过程调用了vsprintf和write函数,接受一个格式串之后将匹配到的参数按照格式串的形式输出。vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt,用格式字符串对个数变化的参数进行格式化,产生格式化输出。vsprintf函数生成显示信息到write系统函数,到陷阱-系统调用 int 0x80或syscall。字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点。

8.4 getchar的实现分析

getchar代码如下:

图 92 getchar函数

当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码转换成ASCII码,保存到系统的键盘缓冲区中。

getchar调用read系统函数,read通过syscall(系统调用)调用内核中的系统函数,读取存储在键盘缓冲区中的ASCII码,直到读入回车符,然后返回整个字符串,getchar函数只从中读取第一个字符,其他字符被存放在输入缓冲区。

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

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

8.5本章小结

本章介绍了Linux的IO设备管理方法,以及Unix IO接口和函数,同时分析了printf函数和getchar函数的实现方法,体会到Unix IO接口在Linux系统中的重要作用,同时也了解了作为异步异常之一的键盘中断的处理。

结论

用计算机系统的语言,逐条总结hello所经历的过程。

你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。

  1. 程序员通过编辑器等进行编写,将hello的源代码写入,存为.c文件,hello.c源程序诞生。
  2. 预处理。hello.c文件通过预处理器cpp将调用的库,所有的宏替换掉,合并到hello.i的文本文件中。
  3. 编译。在编译器中经过编译,产生了hello.s。
  4. 汇编。经过汇编器as将汇编文件转换为机器语言的可重定位二进制目标文件hello.o,此时的hello.o能够被机器识别但是由于不完整无法被执行。
  5. 链接。通过符号解析与重定位,将可重定位目标文件与所依赖的模块链接起来,产生了可以被机器执行的可执行文件hello。
  6. 运行。在shell中输入命令运行。
  7. shell调用fork函数为hello创建子进程,在子进程中调用execve函数, 启动加载器,映射虚拟内存,进入程序入口之后开始载入物理内存,然后进入main函数。
  8. CPU为其分配时间片,在每一个时间片中hello享有私有的地址空间,独立执行自己的逻辑控制流。
  9. CPU访问hello的时候,请求一个虚拟地址,MMU把虚拟地址转换成物理地址,malloc动态申请内存。
  10. hello在运行中可以收到来自键盘输入的信号,比如CTRL-C,CTRL-Z等。
  11. hello子进程收到终止信号后就死亡了,在父进程没有调用waitpid函数将其回收前,他一直是僵死进程。
  12. shell父进程调用waitpid函数将其回收,内核删除了为hello进程创建的所有数据结构。

计算机系统这门课程很复杂,从一个很简单的程序hello.c产生到消失经历了复杂的过程,从中我也学习到了很多知识,通过实验以及大作业使我更深入的理解了理论知识,使我收获了很多。通过对这门课程的学习,我已经理解了一些计算机系统的知识,但在后续的学习中仍需不断深入理解,才能不断提升自己对计算机系统的理解,提升自己的能力。

附件

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

(1)hello.c:源代码

(2)hello.i:预处理后的文本文件

(3)hello.s:编译后的汇编文件

(4)hello.o:汇编后的可重定位目标执行文件

(5)hello:链接之后的可执行文件

(6)hello_elf.txt:用readelf读取hello.o得到的ELF格式信息

(7)hello1_elf.txt:用readelf读取hello得到的ELF格式信息

(8)hello.txt:hello反汇编的结果

(9)hello_o.txt:hello.o反汇编的结果

参考文献

[1] Randal E.Bryant, David R.O'Hallaron.深入理解计算机系统[M]. 北京:机械工业出版社,2016.

[2] http://t.csdnimg.cn/jJrbB

[3] http://t.csdnimg.cn/jozdZ

[4] http://t.csdnimg.cn/DfSvX

[5] 袁春风. 计算机系统基础. 北京:机械工业出版社,2014

[6] https://www.cnblogs.com/pianist/p/3315801.html

  • 11
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值