摘  要

本论文结合csapp课本研究了hello.c这一简单c语言文件在Linux系统下从诞生到结束的整个生命周期,以其原始程序开始,依次深入研究了预处理、编译、链接、加载、运行、终止和回收的过程,深入了解了hello.c的“一生”。通过对hello.c的漫游对计算机系统知识有了更深刻的了解。

关键词:计算机系统;程序生命周期;Linux;hello程序

(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分

目  录

第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的预处理结果解析... - 6 -

2.4 本章小结... - 7 -

第3章 编译... - 8 -

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

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

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

3.3.1按文件结构分析... - 8 -

3.3.2数据类型... - 9 -

3.3.3赋值操作... - 10 -

3.3.4类型转换... - 10 -

3.3.5算术操作... - 10 -

3.3.6关系操作... - 11 -

3.3.7数组/指针/结构操作... - 11 -

3.3.8控制转移... - 11 -

3.3.9函数操作... - 12 -

3.4 本章小结... - 14 -

第4章 汇编... - 15 -

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

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

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

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

4.5 本章小结... - 19 -

第5章 链接... - 20 -

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

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

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

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

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

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

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

5.8 本章小结... - 28 -

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

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

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

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

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

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

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

6.7本章小结... - 35 -

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

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

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

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

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

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

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

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

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

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

7.10本章小结... - 43 -

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

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

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

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

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

8.5本章小结... - 47 -

结论... - 48 -

附件... - 49 -

参考文献... - 50 -

第1章 概述

1.1 Hello简介

根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。

Hello的P2P(from program to process)就是说hello.c文件从可执行程序(Program)逐步变为进程(Process)的整个过程。在Linux系统下,hello.c文件依次经过cpp(C Pre-Processor,C预处理器)预处理产生hello.c,ccl(C Compiler,C编译器)编译产生hello.s,as(Assembler,汇编器)汇编c产生hello.o,ld(Linker,链接器)链接最终成为可执行目标程序hello(在Linux下该文件无固定后缀)。打开shell,输入命令./hello后,shell 通过fork产生子进程,hello 便从可执行程序(Program)变成为进程(Process)。

图 1 hello的各个阶段

Hello的020就是说hello.c文件“From 0 to 0”,初始时内存中并无hello文件的相关内容,这便是“From 0”。子进程调用execve函数,为hello进行内存映射,设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。进入程序入口后通过存储管理机制将指令和数据载入内存,CPU以流水线形式读取并执行指令,执行逻辑控制流。操作系统负责进程调度,为进程分时间片。执行过程中通过L1、L2、L3高速缓存等进行存储管理,通过I/O系统进行输入输出。当程序运行结束后,shell回收hello进程,删除和该进程相关的内容,这时hello进程就不存在了,这即为“to 0”。

1.2 环境与工具

列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。

硬件:

1.    CPU:inter core i5 1035G1 CPU @ 1.00GHz

2.    RAM:16.00GB 系统类型:64位操作系统,基于x64的处理器

软件:

windows11 64位,VMware® Workstation 15 Pro,Ubuntu 20.04

开发与调试工具:

Visual Studio2022 64bit、gcc、gedit、edb、readef

1.3 中间结果

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

文件名称

文件作用

hello.i

预处理之后文本文件

hello.s

编译之后的汇编文件

hello.o

汇编之后的可重定位目标执行

hello

链接之后的可执行目标文件

helloo.objdmp

Hello.o 的反汇编代码

hello.elf

Hello.o 的 ELF 格式

hello.objdmp

Hello 的反汇编代码

hello2.elf

Hello的 ELF 格式

1.4 本章小结

本章主要介绍了Hello自白中P2P和020的含义,实验所需的环境和工具,以及中间结果。

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

概念:预处理器(cpp)根据以字符开头#的命令,修改原始的C程序。比如hello.c中第一行的#include<sdtio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入到程序文本中。结果就得到了另一个C程序,通常是以.i作为文件扩展名。

作用:预处理是在进行编译的第一遍扫描(词法扫描和语法分析)之前所作的工作。预处理是C语言的一个重要功能,它由预处理程序负责完成。当对一个源文件进行编译时,系统把自动引用预处理程序对源程序中的预处理部分作处理,处理完毕自动进入对源程序的编译。C语言提供了多种预处理功能,如宏定义、文件包含、条件编译等。合理地使用预处理功能编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。

2.2在Ubuntu下预处理的命令

在Ubuntu下预处理的命令:gcc Hello.c -E -o Hello.i

运行截图如下:

图 2 预处理指令

2.3 Hello的预处理结果解析

打开hello.i发现程序扩展到了3060行,行数大幅增加,而main函数出现在3047行之后。

图 3 hello.i内容

在这之前出现的是三个头文件 stdio.h unistd.h stdlib.h 的依次展开。展开的过程如下(以 stdio.h 的展开为例):stdio.h 是标准库文件,cpp到默认的环境变量下寻找 stdio.h,打开文件/usr/include/stdio.h ,若stdio.h文件中使用了#define语句,则按照上述流程继续递归地展开,直到所有#define语句都被解释替换掉为止。除此之外,CPP还会进行删除程序中的注释和多余的空白字符等操作,并对一些值进行替换。

2.4 本章小结

本章主要讲述了预处理的概念和作用,Ubuntu下预处理的命令,以及结合实际程序对预处理结果的解析。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

概念:编译程序所要作得工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。编译器(gcc)将文本文件 hello.i 翻译成文本文件 hello.s,它包含一个汇编语言程序。

作用:将文本文件翻译成汇编语言程序,为后续将其转化为二进制机器码做准备。

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

3.2 在Ubuntu下编译的命令

在Ubuntu下编译的命令:gcc -S hello.i -o hello.s

图 4 编译命令

3.3 Hello的编译结果解析

3.3.1按文件结构分析

hello.s文件的整体结构

指令

含义

.file

源文件

.text

代码段

.section .rodata

只读变量

.globl

声明一个全局变量

.type

用来指定是函数类型或是对象类型

.size

声明大小

.long、.string

声明一个 long、string 类型

.align

声明对指令或者数据的存放地址进行对齐的方式

3.3.2数据类型

1.常量

在hello.c源程序中有两个printf的参数是字符串常量,分别为"Usage: Hello 学号 姓名!\n"和"Hello %s %s\n"。在编译生成的hello.s中可以看到,这两个字符串常量分别由.LC0和.LC1指示,都存放在只读数据段.rodata中。

图 5 编译结果截图1

2.局部变量

编译器将局部变量存储在寄存器或者栈空间中。在hello.s中编译器将i存储在栈空间-4(%rbp)中。

图 6 编译结果截图2

3.int argc

argc是main函数的参数之一,64位编译下,由寄存器传入,进而保存在堆栈中。

4.数组

程序中涉及的数组为char *argv[],即函数的第二个参数。在hello.s中,数组的首地址保存在栈中。访问时,通过寄存器寻址的方式访问。

图 7 编译结果截图3

3.3.3赋值操作

程序中仅有一个赋值操作i=0,使用mov指令完成,而使用不同的后缀可以对不同大小的值进行操作。

后缀

b

w

l

q

大小(字节)

1

2

3

4

3.3.4类型转换

hello.s中只出现了一次类型转换:使用atoi函数将字符串(输入的秒数)转换成对应的整型数。

3.3.5算术操作

汇编语言中算术操作指令有:

图 8 编译结果截图4

在hello.s中应用的算术操作有:

1.    subq      $32, %rsp:开辟栈帧

2.    addq      $8, %rax(或者$16,%rax):修改地址偏移量

3.    addl $1, -4(%rbp):实现i++

图 9 编译结果截图5

3.3.6关系操作

hello.c程序中,一共出现了两次关系操作:

(1)argc!=4:判断argc不等于4。hello.s中使用cmpl $4,-20(%rbp),计算argc-4然后设置条件码,再je根据条件码决定是否跳转。

图 10 编译结果截图6

(2)i<8:检查i是否小于8。在hello.s中,使用cmpl $8, -4(%rbp)比较i与7的大小,然后设置条件码,再jle根据条件码决定是否跳转。

图 11 编译结果截图7

3.3.7数组/指针/结构操作

源代码中出现的数组主要是argv[],在向main函数传递参数时,通过movq %rsi, -32(%rbp)进行参数的传递,将argv数组的首地址保存在栈中。在后面的循环中,读取了argv中的元素,分别读取了argv[1]和argv[2],这是通过下面的汇编代码实现的。

图 12 编译结果截图8

3.3.8控制转移

hello.c源程序中出现了两次控制转移:

1.if(argc!=4):if判断argc的取值后的控制转移。编译时使用cmpl指令将argc和4进行比较,并设置条件码。跳转指令je根据条件码决定是否跳转。控制转移由指令je完成。

图 13 编译结果截图9

2.for(i=0;i<8;i++):通过每次判断i是否满足小于8来判断是否需要跳转至循环语句中,使用cmpl指令将i与7进行比较,并设置条件码,跳转指令jle根据条件码决定是否跳转。控制转移由指令jle完成。

图 14 编译结果截图10

3.3.9函数操作

C语言中,子程序的作用是由一个主函数和若干个函数构成。由其主函数调用其他函数,其他函数也可以互相调用。在程序设计中,常将一些常用的功能模块编写成函数,放在函数库中供公共选用。利用函数可以减少重复编写程序段的工作量。

函数包括如下内容:

  1. 函数表达式:函数作为表达式中的一项出现在表达式中,以函数返回值参与表达式的运算。这种方式要求函数是有返回值的。
  2. 函数语句:函数调用的一般形式加上分号即构成函数语句。
  3. 函数实参:函数作为另一个函数调用的实际参数出现。这种情况是把该函数的返回值作为实参进行传送,因此要求该函数必须是有返回值的。

调用函数的动作如下:

  1. 传递控制:进行过程 Q 的时候,程序计数器必须设置为 Q 的代码的起始地址,然后在返回时,要把程序计数器设置为 P 中调用 Q 后面那条指令的地址。
  2. 传递数据:P 必须能够向 Q 提供一个或多个参数,Q 必须能够向 P 中返回一个值。
  3. 分配和释放内存:在开始时,Q 可能需要为局部变量分配空间,而在返回前,又必须释放这些空间。

64 位程序参数存储顺序:

1

2

3

4

5

6

7 及以上

%rdi

%rsi

%rdx

%rcx

%r8

%r9

hello.c程序中涉及到以下程序:

(1)main函数

函数调用:main 函数被系统启动函数__libc_start_main 调用,call 指令将下一条指令的地址 dest 压栈,然后跳转到 main 函数。

参数传递:向main函数传递的参数是argc和argv,分别使用%rdi(%edi)和%rsi存储。

函数返回:函数设置%eax为0后就正常退出,使用leave退出。

(2)printf函数

第一次函数调用:这一次printf函数在具体的汇编代码中被优化为puts函数。

第一次参数传递:首先将rdi赋值为字符串“Usage: Hello 学号 姓名! \n”字符串的首地址(leaq .LC0(%rip), %rdi),然后调用了puts函数,将字符串参数传入。

图 15 编译结果截图11

第二次函数调用:这次是直接调用printf函数。

第二次参数传递:显然这里需要传递3个参数,%rdi保存的是“Hello %s %s\n”的首地址,%rsi保存的是argv[1],%rdx保存的是argv[2]。

图 16 编译结果截图12

函数返回:

(3)atoi函数

函数调用:通过汇编语句call atoi@PLT调用。

参数传递:通过movq   %rax, %rdi完成参数传递,%rid保存的是argv[3]。

(4)sleep函数

函数调用:通过汇编语句call sleep@PLT调用。

参数传递:通过movl    %eax, %edi完成参数传递,%rid保存的是atoi(argv[3])。

(5)getchar函数

函数调用:通过汇编语句call getchar@PLT调用。

参数传递:getchar函数无需参数传递。

(6)exit函数

函数调用:通过汇编语句call exit@PLT调用。

参数传递:通过汇编语句movl $1,%edi将%edi寄存器内容设置为1。

3.4 本章小结

本章主要讲了编译的概念和作用,以及实现编译的指令,并且以hello.s为例行对数据、赋值、类型转换、算术操作、关系操作、数组/指针/结构操作以及控制转移和函数操作进行了详细的分析和研究。

(第32分)

第4章 汇编

4.1 汇编的概念与作用

概念:汇编是指汇编器(assembler)将以.s结尾的汇编程序翻译成机器语言指令,并把这些指令打包成可重定位目标程序格式,最终结果保存在.o目标文件中的过程。

作用:将以.s结尾的汇编程序翻译成机器语言指令,并把这些指令打包成可重定位目标程序格式,最终结果保存在.o目标文件中。

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

4.2 在Ubuntu下汇编的命令

在Ubuntu下汇编的命令:as hello.s -o hello.o

图 17 汇编命令

4.3 可重定位目标elf格式

首先,在shell中输入readelf -a hello.o > hello.elf 指令获得 hello.o 文件的 ELF 格式:

图 18 产生ELF文件

hello.o文件的ELF的组成分析如下:

1.ELF 头:以 16字节的序列 Magic 开始,Magic 描述了生成这个文件的系统的字的大小和字节顺序,ELF 头其他的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括 ELF 头的大小、目标文件的类型、机器类型(如X86-64)、字节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量等信息。

图 19 ELF文件结果截图1

2.节头:也就是节头部表,包含了文件中出现的各个节的语义,包括节的类型、地址、大小、偏移量等信息。

图 20 ELF文件结果截图2

3.重定位节.rela.text:一个.text节中位置的列表,包含.text节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置,偏移量表示需要进行重定向的代码在.text或.data节中的偏移位置。其中,8 条重定位信息分别是对.L0(第一个 printf中的字符串)、puts函数、exit 函数、.L1(第二个 printf 中的字符串)、printf函数、atoi函数、sleep 函数、getchar 函数进行重定位声明。

每个重定位条目包括:

offset:需要被修改的引用的节偏移;

symbol:标识被修改引用应该指向的符号;

type:重定位类型,告知链接器如何修改新的引用;

attend:一些重定位要使用它对被修改引用的值做偏移调整;

name:重定向到的目标的名称。

ELF定义了32种不同的重定位类型,两种最基本的重定位类型包括R_X86_64_PC32(重定位使用32位PC相对地址的引用)和R_X86_64_32(重定位使用32位绝对地址的引用)。

图 21 ELF文件结果截图3

4.符号表(Symbol Table):存放程序中定义和引用的函数和全局变量的信息。name是符号名称,对于可冲定位目标模块,value是符号相对于目标节的起始位置偏移,对于可执行目标文件,该值是一个绝对运行的地址。size是目标的大小,type是数据或者是函数。Bind字段表明符号是本地的还是全局的。

图 22 ELF文件结果截图4

分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。

4.4 Hello.o的结果解析

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

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

反汇编结果如下:

图 23 hello.o反汇编结果

与hello.s对比可知,除去格式上的差别外,二者总体上是相似的,主要差别如下:

1.立即数的变化:hello.s中的立即数都是用10进制数表示的。但是在objdump的结果中,由于转换成了二进制代码,因此立即数都是用16进制数表示的。

图 24 hello.s结果截图1

图 25 hello.o反汇编结果截图1

2.分支转移的不一致:在hello.s中,跳转指令的目标地址直接记为段名称,如.L2,.L3等。而在反汇编得到的结果中,跳转的目标为具体的地址,在机器代码中体现为目标指令地址与当前指令下一条指令的地址之差。

图 26 hello.s结果截图2

图 27 hello.o反汇编结果截图2

3.函数调用的不一致:hello.s中的函数调用直接在call指令后面加上要调用的函数名。 但是在机器语言中,call指令后是被调函数的PC相对地址。在这里,由于调用的函数都是库函数,需要在动态链接后才能确定被调函数的确切位置,因此call指令后的二进制码为全0,同时需要在重定位节中添加重定位条目,在链接时确定最终的相对地址。

图 28 hello.s结果截图3

图 29 hello.o反汇编结果截图3

4.5 本章小结

本章介绍了汇编的概念与作用,在Ubuntu下通过指令将hello.s文件转换为hello.o文件,生成了相应的ELF文件并进行分析,以及对objdump反汇编生成的文件与hello.s文件进行比较,以此了解了机器语言和汇编语言的异同之处。

(第41分)

第5章 链接

5.1 链接的概念与作用

概念:链接(linking)是将各种代码和数据片段收集并组合为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时,也就是在源代码被编译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至于执行于运行时,也就是由应用程序来执行。

作用:链接使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,我们可以独立的修改和编译那些更小的模块,这也更加便于我们维护和管理我们的代码。

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

5.2 在Ubuntu下链接的命令

在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

链接过程如下:

图 30 链接命令

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

分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。

使用 readelf -a hello > hello.elf 命令生成 hello 程序的 ELF 格式文件。(为与hello.o的ELF文件区分,hello程序的ELF文件命名为hello2.elf)

图 31 生成hello的ELF文件

hello的ELF分析如下:

1.ELF 头:以16B的序列Magic开始,Magic描述了生成该文件的系统的字的大小和字节顺序,ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型、机器类型、字节头部表的文件偏移,以及节头部表中条目的大小和数量等信息。

与链接前的ELF 头比较,基本信息未发生改变(如Magic,类别等),而类型发生改变,程序头大小和节头数量增加,并且获得了入口地址。

图 32 ELF头

2.节头:hello2.elf中的节头包含了文件中出现的各个节的语义,包括节的类型、位置、偏移量和大小等信息。与链接前的hello.elf相比,其在链接之后的内容更加详细(多了13个节)。

图 33 节头

3.程序头部表描述了可执行文件的连续的片映射到连续的内存段的映射关系。包括目标文件的偏移、段的读写/执行权限、内存的开始地址、对齐要求、段的大小、内存中的段大小等。

图 34 程序头

4.Dynamic section:如果目标文件参与动态链接,则其程序头表将包含一个类型为 PT_DYNAMIC 的元素。此段包含 .dynamic 节。特殊符号 _DYNAMIC 用于标记包含以下结构的数组的节。

图 35 Dynamic section

5.重定位节.rela.text:一个.text 节中位置的列表,包含.text节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。六条重定位信息,分别描述了原hello的函数main、标准头文件的函数puts、函数printf、函数getchar、函数exit、函数sleep的重定位声明。

图 36 重定位节

6.符号表:符号表存放程序中定义和引用的函数和全局变量的信息,每个符号表是一个条目的数组,每个条目包括value:距定义目标的节的起始位置的偏移;size:目标的大小;type:指明数据还是函数;bind:表示符号是本地的还是全局的等等。hello的符号表一共描述了51个符号,比hello.o多出33个符号。多出的符号都是链接后产生的库中的函数以及一些必要的启动函数。(下图只截取了部分符号表)

图 37 符号表

5.4 hello的虚拟地址空间

使用edb加载hello,可以看到进程的虚拟地址空间各段信息。可以看出,段的虚拟空间从0x400000开始,到0x400fff结束。

图 38 虚拟地址截图1

通过观察edb的Symbols小窗口,我们发现从虚拟地址从0x400000开始和5.3节中的节头表几乎是一一对应的。

图 39 虚拟地址截图2

5.5 链接的重定位过程分析

objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。

结合hello.o的重定位项目,分析hello中对其怎么重定位的。

使用objdump -d -r hello>hello.objdump指令生成hello的反汇编文件

图 40 hello反汇编指令

下面是对hello.o和hello反汇编后结果的一些对比:

1.helloo.objdump中的大多是相对偏移地址,而hello.objdump文件中的地址是虚拟地址。在链接过程中,链接器解析了重定位条目,并计算相对距离,修改了对应位置的字节代码为PLT 中相应函数与下条指令的相对地址,从而得到完整的反汇编代码。

图 41 地址区别

2.hello.objdump比helloo.objdump多了许多节,不仅有很多helloo.objdump没有的节(例如.init节,.plt节等),而且.text节中的内容也比之前更多。

图 42 内容上的区别

3.hello.objdump中增加了许多外部链接的共享库函数,多出了puts@plt,printf@plt,getchar@plt,atoi@plt,exit@plt,sleep@plt等函数的代码。这是因为动态链接器将共享库中hello.c用到的函数加入可执行文件中。

图 43 调用外部库函数

根据以上分析我们可以看出,链接过程会扫描分析所有相关的可重定位目标文件,并完成两个主要任务:首先进行符号解析,将每个符号引用与一个符号定义关联起来;随后进行重定位,链接器使用汇编器产生的重定位条目的详细指令,把每个符号定义与一个内存位置关联起来。最终的结果是将程序运行所需的各部分组装在一起,形成一个可执行目标文件。

5.6 hello的执行流程

程序名称

程序地址

hello!_start

0x4010f0

libc-2.31.so!__libc_start_main

0x7f5c62888fc0

-libc-2.31.so!__cxa_atexit

0x7f5c628abe10

hello!__libc_csu_init

0x4011c0

hello!_init

0x401000

libc-2.31.so!_setjmp

0x7f5c628a7ca0

-libc-2.31.so!_sigsetjmp

0x7f5c628a7be0

hello!main

0x401125

hello!puts@plt

0x401030

hello!exit@plt

0x401070

*hello!printf@plt

--

*hello!sleep@plt

--

*hello!getchar@plt

--

libc-2.31.so!exit

0x7f5c928aba70

5.7 Hello的动态链接分析

由于无法预测函数的运行时地址,对于动态共享链接库中 PIC 函数,编译器需要添加重定位记录,等待动态链接器处理。链接器采用延迟绑定的策略,防止运行时修改调用模块的代码段。

动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。.got与.plt节保存着全局偏移量表GOT

.got地址为0x00403ff0

.plt地址为0x404000

通过edb查看,在dl_init调用前,其内容如下:

图 44 调用前

调用后,内容如下:

图 45 调用后

5.8 本章小结

本章主要介绍了链接的概念与作用、hello的ELF格式,分析了hello的虚拟地址空间、重定位过程、执行流程和动态链接过程。

(第51分)

第6章 hello进程管理

6.1 进程的概念与作用

定义:进程是一个执行中的程序的实例,每一个进程都有它自己的地址空间,一般情况下,包括文本区域、数据区域、和堆栈。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储区着活动过程调用的指令和本地变量。

作用:给应用程序提供了两个关键抽象:

1.   一个独立的逻辑控制流,提供一个假象,好像程序独占地使用处理器

2.   一个私有地址空间,提供一个假象,好像程序独占地使用内存系统

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

作用:Shell-bash是一个交互型应用级程序,代表用户运行其他程序。它是系统的用户界面,提供了用户与内核进行交互操作的一种接口。它接收用户输入的命令并把它送入内核去执行。

处理流程:shell首先检查命令是否是内部命令,若不是再检查是否是一个应用程序(这里的应用程序可以是Linux本身的实用程序,如ls和rm,也可以是购买的商业程序,如xv,或者是自由软件,如emacs)。然后shell在搜索路径里寻找这些应用程序(搜索路径就是一个能找到可执行程序的目录列表)。如果键入的命令不是一个内部命令并且在路径里没有找到这个可执行文件,将会显示一条错误信息。如果能够成功找到命令,该内部命令或应用程序将被分解为系统调用并传给Linux内核。

简化处理流程:

  1. 从终端读入输入的命令;
  2. 将输入字符串切分获得所有的参数;
  3. 如果是内置命令则立即执行;
  4. 若不是则调用相应的程序执行;
  5. shell应该随时接受键盘输入信号,并对这些信号进行相应处理。

6.3 Hello的fork进程创建过程

打开Shell,输入命令./hello 120L020403 刘洲 1,带参数执行生成的可执行文件。

过程:

1.    hello 不是一个内置的shell命令,所以解析之后终端程序判断./hello的语义为执行当前目录下的可执行目标文件hello。

2.    之后终端程序首先会调用 fork 函数创建一个新的运行的子进程,新创建的子进程几乎父进程相同,但不完全与相同。

3.    父进程与子进程之间最大的区别在于它们拥有不同的 PID。子进程得到与父进程用户级虚拟地址空间相同的一份副本,当父进程调用 fork 时,子进程可以读写父进程中打开的任何文件。

4.    内核能够以任意方式交替执行父子进程的逻辑控制流的指令,父进程与子进程是并发运行而独立的。在子进程执行期间,父进程默认选项是显示等待子进程的完成。当子进程运行结束时,父进程如果仍然存在,则执行对子进程的回收,否则就由init进程回收子进程。

图 46 Shell结果

6.4 Hello的execve过程

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

6.5 Hello的进程执行

结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。

进程提供给应用程序的抽象:

一个独立的逻辑控制流,它提供一个假象,好像我们的进程独占的使用处理器。

一个私有的地址空间,它提供一个假象,好像我们的程序独占的使用CPU内存。

hello进程的执行是依赖于进程所提供的抽象的基础上,下面阐述操作系统所提供的的进程抽象:

  1. 逻辑控制流:一系列程序计数器 PC 的值的序列叫做逻辑控制流,在同一个处理器核心中,每个进程执行它的流的一部分后被抢占(暂时挂起),然后轮到其他进程,进程轮流使用处理器,。
  2. 时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
  3. 用户模式和内核模式:处理器通常使用一个寄存器描述了进程当前享有的特权,对两种模式区分。设置模式位时,进程处于内核模式,该进程可以访问系统中的任何内存位置,可以执行指令集中的任何命令;当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据。
  4. 上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态。它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。

hello sleep进程调度的过程:

1.   当调用 sleep 之前,如果 hello 程序不被抢占则顺序执行,假如发生被抢占的情况,则进行上下文切换

2.   上下文切换是由内核中调度器完成的,当内核调度新的进程运行后,它就会抢占当前进程,并进行

a)   保存以前进程的上下文

b)   恢复新恢复进程被保存的上下文,

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

3.   hello初始运行在用户模式,在hello进程调用sleep之后陷入内核模式,内核处理休眠请求主动释放当前进程,并将 hello 进程从运行队列中移出加入等待队列

4.   定时器开始计时,内核进行上下文切换将当前进程的控制权交给其他进程,当定时器到设定的时间时发送一个中断信号,

5.   进入内核状态执行中断处理,将 hello 进程从等待队列中移出重新加入到运行队列,成为就绪状态,hello进程就可以继续进行自己的控制逻辑流了。

图 47 进程调度

6.6 hello的异常与信号处理

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

hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。

程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps  jobs  pstree  fg  kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。

hello程序可能出现的四种异常分别是:

中断:在hello程序执行的过程中可能会出现外部I/O设备引起的异常。

陷阱:陷阱是有意的异常,是执行一条指令的结果,hello执行sleep函数的时候会出现这个异常

故障:在执行hello程序的时候,可能会发生缺页故障。

终止:终止时不可恢复的错误,在hello执行过程可能会出现DRAM或者SRAM位损坏的奇偶错误。

hello执行过程中的信号及处理:

   SIGINT:键入Ctrl-C后内核向hello进程发送,终止程序。

 SIGSTP:键入Ctrl-Z后内核向hello进程发送,停止直到下一个SIGCONT。

   SIGCONT:键入fg后内核向hello进程发送,若停止则继续执行。

   SIGKILL:键入kill -9 <PID>后内核向hello进程发送,终止程序。

1.正常执行hello程序的结果:当程序执行完成之后(以键入回车结束),进程回收。

图 48 正常执行

2.在程序打印两条信息之后按下ctrl-z :

当按下 ctrl-z之后,shell的父进程收到 SIGSTP 信号,信号处理函数的逻辑是打印屏幕回显、将hello进程挂起。

通过 ps 命令我们可以看出 hello 进程其实没有被回收,此时他的后台job号是1。

通过调用fg 1将其调到前台,此时shell 程序首先打印hello的命令行命令,hello继续运行打印剩下的6条信息,之后键入回车,程序结束,同时进程被回收。

图 49 输入ctrl-z

暂停后,可以通过kill指令杀死进程

图 50 使用kill指令

3.在程序打印2条信息之后按下ctrl-c :当按下ctrl-c之后,shell父进程收到SIGINT信号,信号处理函数的逻辑是结束hello,并回收hello进程。

图 51 输入ctrl-c

4.在程序运行时按回车,会多打印几处空行,程序可以正常结束。

图 52 输入回车

5.在程序运行中途不断乱按的结果:乱按只是将屏幕的输入缓存到 stdin,当getchar的时候读出一个’\n’结尾的字串(作为一次输入),hello结束后,stdin中的其他字串会当做shell若干条命令行输入。

图 53 不断乱按

pstree(部分)如下:

图 54 pstree

6.7本章小结

本章主要阐述了进程的定义与作用,介绍了Shell的一般处理流程,调用 fork 创建新进程,调用 execve 执行 hello的过程,hello的进程执行大致过程,并对hello的异常与信号处理进行了分析。

(第61分)

第7章 hello的存储管理

7.1 hello的存储器地址空间

1.逻辑地址

逻辑地址(Logical Address)是指由程序产生的与段相关的偏移地址部分,是相对应用程序而言的,如hello.o中代码与数据的相对偏移地址。

2.线性地址

线性地址(Linear Address)是逻辑地址经过段机制转化后形成的地址,其为处理器可寻址空间的地址,用于描述程序分页信息的地址。具体以hello而言,线性地址标志着 hello 应在内存上哪些具体数据块上运行。

3.虚拟地址

虚拟地址(Virtual Address)是程序运行在保护模式下,程序访问存储器所使用的逻辑地址称为虚拟地址。在保护模式中,在程序从磁盘加载进内存的中间加了一个中间层,即就是虚拟地址,在程序编译,链接的时候先映射进虚拟地址,在运行的时候会再映射进物理地址。这样的好处在于,在虚拟地址中,hello程序的虚拟地址,不管通过如何偏移,它都在虚拟地址中,最后再映射进物理地址,不会影响到其他的程序,起到了进程隔离,保护了其他的进程。

4.物理地址

物理地址(Physical Address)是指出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址(hello程序运行时代码、数据等对应的可用于直接在内存中寻址的地址)。如果启用了分页机制,那么线性地址就会使用页目录和页表中的项变换成物理地址;如果没有启用分页机制,那么线性地址就直接成为物理地址了。

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

为了运用所有的内存空间,Intel 8086设定了四个段寄存器,专门用来保存段地址:CS(Code Segment):代码段寄存器;DS(Data Segment):数据段寄存器;SS(Stack Segment):堆栈段寄存器;ES(Extra Segment):附加段寄存器。

当一个程序要执行时,就要决定程序代码、数据和堆栈各要用到内存的哪些位置,通过设定段寄存器CS,DS,SS来指向这些起始位置。通常是将DS固定,而根据需要修改CS。所以,程序可以在可寻址空间小于64K的情况下被写成任意大小。所以,程序和其数据组合起来的大小,限制在DS所指的64K内,这就是COM文件不得大于64K的原因。

段寄存器是因为对内存的分段管理而设置的。

计算机需要对内存分段,以分配给不同的程序使用(类似于硬盘分页)。在描述内存分段时,需要有如下段的信息:1.段的大小;2.段的起始地址;3.段的管理属性(禁止写入/禁止执行/系统专用等)。

1.保护模式(如今大多数机器已经不再支持):

段寄存器的唯一目的是存放段选择符,其前13位是一个索引号,后面3位包含一些硬件细节(还有一些隐藏位,此处略)。

寻址方式为:以段选择符作为下标,到GDT/LDT表(全局段描述符表(GDT)和局部段描述符表(LDT))中查到段地址,段地址+偏移地址=线性地址。

2.实模式:

段寄存器含有段值,访问存储器形成物理地址时,处理器引用相应的某个段寄存器并将其值乘以16,形成20位的段基地址,段基地址·段偏移量=线性地址。

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

线性地址(VA)转换为物理地址(PA)是通过对虚拟地址存储空间进行分页来完成的。

通过第7.2节中的段式管理过程,可以获得一个线性地址/虚拟地址,该地址记录为VA。 虚拟地址可以分为两个部分:VPN(虚拟页码)和VPO(虚拟页面偏移)。 根据计算机系统的特性,可以确定VPN和VPO的特定数字。 由于虚拟内存和物理内存的页面大小相同,因此VPO与PPO一致(物理页面偏移)。需要通过页面表中的页面表条目(PTE)获得PPN(物理页码),如下图所示。

图 55 页式管理

若PTE的有效位为1,则发生页命中,可以直接获取到物理页号PPN,PPN与PPO共同组成物理地址。

如果PTE的有效位为0,则表示相应的虚拟页面不会缓存到物理内存中,并且会生成页面故障。 调用操作系统内核的页面以确定牺牲页面并调整新页面。 返回原始过程,调用再次导致页面的指令。 此时页面命中,获得PPN并使用PPO形成物理地址。

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

TLB的支持:在MMU中,它包括有关PTE的缓存,称为翻译后备缓冲区(TLB)。 TLB是一个小的虚拟地址缓存,每行都保留为一个由单个PTE组成的块。 在VA转换为PA期间,您需要使用VPN来确定相应的页面表条目,因此TLB需要通过VPN找到PTE。 像其他缓存一样,需要匹配组索引和行。 如果TLB具有2T组,则TLB的索引TLBI由VPN 的t个最低位组成,而TLB标记TLBT TLBT由VPN中的剩余位组成。

图 56 访问TLB

当MMU翻译地址时,VPN将首先传递给TLB。 查看是否需要TLB中的PTE。 如果TLB命中,则可以直接从TLB获得PTE。 VPO已连接以获取相应的物理地址。 目前,所有地址翻译步骤均在芯片上的MMU中执行,因此非常快。 如果未点击TLB,则在7.3中描述的过程相似,需要从缓存或内存中获取相应的PTE。

四级页面表的支持:多级页表可用于压缩页面表。对于k级页表的级别结构,虚拟地址的VPN分为k,每个VPNI都是II页表的索引。当1≤j≤K-1时,第j级页表中的每个PTE都指向第j+1级页表的底部。 k级页面中的每个PTE都与包含PPN或物理页面的磁盘块地址的多级页表的每个PTE相同。对于Intel Core i7,使用4级页面表,每个VPNi都有9位数字。当TLB无法命中时,将36位VPN分为VPN1,VPN2,VPN3,VPN4,每个VPNI被用作页面表的偏移量。 CR3寄存器包含L1页表的物理地址,VPN1提供了L1 PTE的偏移。该PTE包含L2页表的基础地址。 VPN2在此L2页面中提供了PTE的偏移,依此类推。最后一个L4 PTE包含所需的物理页码,并连接到虚拟地址中的VPO,以获取相应的物理地址。

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

L1 Cache的物理内存访问过程如下:

首先选择虚拟地址的组索引位,找到对应的组。再使用行匹配将虚拟地址的标记和组中各行的标记位比较。命中的条件是高速缓存行有效且标记位匹配。

高速缓存命中后,使用块偏移找到需要字节的块内偏移位置,将其取出返回给CPU。

若出现了不命中的现象,那么需要从L2中寻找所请求的块,若找到就将其存储在cache的一行中。一般使用最近最少使用的替换策略进行替换。若没找到还需要到L3重复以上过程,若L3中仍未找到则在主存中继续重复这个过程。

图 57 地址翻译

7.6 hello进程fork时的内存映射

当父进程(Shell)调用fork函数时,内核会为新过程创建各种数据结构(将来加载执行Hello的过程)并将其分配给唯一的PID。 为了为这个新过程创建虚拟内存,它创建了MM_struct,区域结构和页面表的当前过程。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。

当叉子在新过程中返回时,新过程与调用叉时存在的虚拟内存相同。 当以后写这两个过程中的任何一个中的任何一个时,新页面将在编写写作机制时创建一个新页面。 因此,每也就为每个进程保持了私有地址空间的抽象概念。

7.7 hello进程execve时的内存映射

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

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

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

缺页故障是一种常见的故障,当指令引用一个虚拟地址,在 MMU 中查找页表时发现与该地址相对应的物理地址不在内存中,因此必须从磁盘中取出的时候就会发生故障。

图 58 缺页故障处理

缺页中断处理:缺页处理程序是系统内核中的代码,选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU 重新启动引起缺页的指令,这条指令再次发送 VA 到MMU,此时MMU就能正常翻译VA了。

7.9动态存储分配管理

动态内存管理的基本方法与策略介绍如下:

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

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

具体而言,分配器分为两种基本风格:显式分配器、隐式分配器。

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

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

下面介绍动态存储分配管理中较为重要的概念:

1.隐式链表

堆中的空闲块通过头部中的大小字段隐含地连接,分配器通过遍历堆中所有的块,从而间接遍历整个空闲块的集合。

对于隐式链表,其结构如下:

图 59 隐式链表

2.显式链表

在每个空闲块中,都包含一个前驱(pred)与后继(succ)指针,从而减少了搜索与适配的时间。

显式链表的结构如下:

图 60 显式链表

3.带边界标记的合并

采取使用边界标记的堆块的格式,在堆块的末尾为其添加一个脚部,其为头部的副本。添加脚部之后,分配器就可以通过检查前面一个块的脚部,判断前面一个块的起始位置和状态。从而实现快速合并,减小性能消耗。

4.分离存储

维护多个空闲链表,其中,每个链表的块具有相同的大小。将所有可能的块大小分成一些等价类,从而进行分离存储。

7.10本章小结

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

(第7 2分)

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件

所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,所有的输入输出都被当作对相应文件的读和写来执行。

设备管理:unix io接口

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

8.2 简述Unix IO接口及其函数

Unix I/O 接口统一操作:

  1. 打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个 I/O 设备。内核返回一个小的非负整数,叫做描述符。描述符在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。
  2. Shell 创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误。
  3. 改变当前的文件位置:内核保持着每个打开的文件的一个文件位置k。k初始为0。这个文件位置k表示的是从文件开头起始的字节偏移量。应用程序能够通过执行seek,显式地将改变当前文件位置 k,例如各种fread或fwrite。
  4. 读写文件:读操作就是从文件复制n>0个字节到内存。从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的文件。当k>=m时,触发EOF。写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k=k+n。
  5. 关闭文件:内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。

Unix I/O 函数:

  1. int open(char* filename,int flags,mode_t mode) ,进程通过调用open函数来打开一个存在的文件或是创建一个新文件的(即fopen的内层函数)。open函数将filename(文件名,含后缀)转换为一个文件描述符(C中表现为指针),并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件(读或写或两者兼具),mode参数指定了新文件的访问权限位(只读等)。
  2. int close(fd),fd是需要关闭的文件的描述符(C中表现为指针),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的当前文件位置。

8.3 printf的实现分析

首先看一下printf的源代码:

int printf(const char *fmt, ...)

{

int i;

va_list arg = (va_list)((char *)(&fmt) + 4);

i = vsprintf(buf, fmt, arg);

write(buf, i);

return i;

}

这里va_list是char类型的指针,表示arg是...中的第一个参数的地址。另外,我们发现在printf函数里分别调用了vsprintf和write函数,下面对这两个函数一一分析。

csprintf的代码:

int vsprintf(char *buf, const char *fmt, va_list args)

{

char *p;

chartmp[256];

va_listp_next_arg = args;

for (p = buf; *fmt; fmt++)

{

if (*fmt != '%')

{

*p++ = *fmt;

continue;

}

fmt++;

switch (*fmt)

{

case 'x':

itoa(tmp, *((int *)p_next_arg));

strcpy(p, tmp);

p_next_arg += 4;

p += strlen(tmp);

break;

case 's':

break;

/* 这里应该还有一些对于

其他格式输出的处理 */

default:

break;

}

return (p - buf);

}

}

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

vsprintf函数中也调用了write函数,write函数如下:

write:

mov eax, _NR_write

mov ebx, [esp + 4]

mov ecx, [esp + 8]

int INT_VECTOR_SYS_CALL

在write函数中,将栈中的参数放入寄存器,ecx存放字符个数,ebx存放第一个字符地址,int INT_VECTOR_SYS_CALLA表示通过系统调用sys_call。从这里也可以看出write是一个系统函数。下面是sys_call的汇编代码:

sys_call:

/*

* ecx中是要打印出的元素个数

* ebx中的是要打印的buf字符数组中的第一个元素

* 这个函数的功能就是不断的打印出字符,直到遇到:'\0'

* [gs:edi]对应的是0x80000h:0采用直接写显存的方法显示字符串

*/

xor si,si

mov ah,0Fh

mov al,[ebx+si]

cmp al,'\0'

je .end

mov [gs:edi],ax

inc si

loop:

sys_call

.end:

ret

sys_call函数实现的功能就是把将要输出的字符串从总线复制到显卡的显存中。显存中存储的是字符的ASCII码。字符显示驱动子程序通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。至此,就完成了对printf函数的分析,也完成了字符串的整个输出过程。

8.4 getchar的实现分析

当程序运行至getchar函数时,程序通过系统调用read等待用户键入字符并按回车键(通知系统输入完成),一种getchar函数的实现如下:

#include "sys/syscall.h"

#include <stdio.h>

int getchar(void)

{

char c;

return (read(0,&c,1)==1)?(unsigned char)c:EOF

//EOF定义在stdio.h文件中

}

当用户键入回车之后,getchar通过系统调用read从输入缓冲区中每次读入一个字符。getchar函数的返回值是用户输入的第一个字符的ascii码,如出错返回-1。

异步异常——键盘中断(用户输入)的处理:键盘中断处理子程序接受按键扫描码并转成ASCII码,保存在系统的键盘缓冲区。

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

8.5本章小结

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

(第81分)

结论

虽然hello是一个非常简单的程序,但需要计算机系统各方面的协同工作,经历了复杂的过程,漫游了整个宏大的计算机世界:

  1. 预处理:hello.c源代码文件通过C语言预处理器的预处理,将hello.c中include的所有外部的头文件头文件内容直接插入程序文本中,完成字符串的替换,得到了调整、展开后的ASCII文本文件hello.i;
  2. 编译:通过词法分析和语法分析,将合法指令翻译成等价的汇编代码。通过编译过程,编译器将hello.i 翻译成汇编语言文件 hello.s;
  3. 汇编:将hello.s汇编程序翻译成机器语言指令,并把这些指令打包成可重定位目标程序格式,然后可以得到可重定向目标文件hello.o;
  4. 链接:通过链接器,将hello的程序编码与动态链接库等整合成为一个单一文件,生成可执行的目标文件hello;
  5. 运行:用户在shell-bash中键入执行hello程序的命令后,shell-bash解释用户的命令,找到hello可执行目标文件并为其执行fork创建新进程,fork得到的新进程通过调用execve完成在其上下文中对hello程序的加载,hello开始执行;
  6. 运行程序:shell调用execve,execve调用启动加载器,加映射虚拟内存,进入程序入口后程序,开始载入物理内存,然后CPU进入main函数执行程序。
  7. 执行指令:CPU为hello分配时间片,在一个时间片中,hello享有CPU资源,顺序执行自己的控制逻辑流;
  8. 访问内存:内存管理单元MMU将逻辑地址,一步步映射成物理地址,进而通过三级高速缓存系统访问物理内存/磁盘中的数据;
  9. 动态申请内存:printf会调用malloc向动态内存分配器申请堆中的内存;
  10. 信号:如果运行途中键入ctr-c、ctr-z,则调用shell的信号处理函数分别停止、挂起;
  11. 结束:shell父进程回收子进程,内核删除为这个进程创建的所有数据结构。

(结论0分,缺失 -1分,根据内容酌情加分)

附件

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

文件名称

文件作用

hello.i

预处理之后文本文件

hello.s

编译之后的汇编文件

hello.o

汇编之后的可重定位目标执行

hello

链接之后的可执行目标文件

helloo.objdmp

Hello.o 的反汇编代码

hello.elf

Hello.o 的 ELF 格式

hello.objdmp

Hello 的反汇编代码

hello2.elf

Hello的 ELF 格式

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

参考文献

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

[2]  C语言预处理程序有什么作用_百度知道

[3]    汇编语言(算术运算类指令总结)_全糖去冰不加料的博客-CSDN博客_汇编语言逻辑运算指令

[4]    atoi_百度百科

[5]    Dynamic Section - Linker and Libraries Guide

[6]    shell(计算机壳层)_百度百科

[7]    UNIX系统文件IO函数_KomaCC的博客-CSDN博客

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

[9]  Linux - shell壳脚本 - TBHacker - 博客园

[10] printf背后的故事 - Florian - 博客园

[11] ELF文件头更详细结构_梦想之家xiao_chen的博客-CSDN博客

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

2022hit计算机系统大作业相关推荐

  1. 计算机系统大作业:Hello's P2P

    计算机系统大作业 题 目 程序人生-Hello's P2P 专 业 计算机科学与技术 指 导 教 师 史先俊 计算机科学与技术学院 2019年12月 摘 要 本文介绍了一个.c源程序在计算机系统中从编 ...

  2. 哈工大2022计算机系统大作业

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算机 指 导 教 师 吴锐 计算机科学与技术学院 2021年5月 摘  要 本文介绍了hello程序的一生. ...

  3. 2022计算机系统大作业——程序人生-Hello’s P2P

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算机 学    号 120L021716 班    级 2003005 学       生 蔡泽栋 指 导 ...

  4. 哈尔滨工业大学计算机系统大作业2022春

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算机 学   号 120L02**** 班   级 2003005 学       生 无敌飞龙 指 导 教 ...

  5. 哈工大csapp计算机系统大作业

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算学部 学   号 120L022009 班   级 2003007 学       生 李沛聪 指 导 教 ...

  6. 哈工大计算机系统大作业

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算机 学   号 120L022123 班   级 2003007 学       生 鹿鸣 指 导 教 师 ...

  7. 哈工大2022计算机系统大作业---程序人生

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算机类 学    号 120L021923 班    级 2003006 学       生 甄自镜 指 导 ...

  8. HIT计算机系统大作业

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算学部 学 号 120L022305 班       级 2003008 学       生 XXX 指 导 ...

  9. 哈工大计算机系统大作业——程序人生-Hello’s P2P

    计算机系统 大作业 题          目程序人生-Hello's P2P 专          业 计算机科学与技术 学       号120L022401 班          级 200300 ...

最新文章

  1. swift_046(Swift map,filter, reduce方法)
  2. js /jquery停止事件冒泡和阻止浏览器默认事件
  3. 使用google云(GCP)二次利用安装kali Linux(kali browser)
  4. 消息中间件—简谈Kafka中的NIO网络通信模型
  5. 在线实时大数据平台Storm单机部署
  6. percona mysql.cnf_Percona MySQL5.6 半同步复制
  7. Python实现ORM
  8. 设计模式学习笔记——原型(Prototype)框架
  9. java 平均分配算法_java 分配算法
  10. 【蓝桥杯单片机】实战训练:基于15单片机的距离测试及电压数据采集响应系统(超声波测距、上位机、DAC输出)
  11. Java实现hsql_java – 从类创建HSQL创建表查询
  12. ubuntu iptables 共享上网
  13. oracle Expdp带条件,Oracle 11g expdp中query参数的使用
  14. 捷联惯导系统学习2.3(方向余弦阵微分方程)
  15. vue2 确认框 MessageBox 弹框 删除确认取消
  16. C# 测试网络速度例子
  17. 麻省理工学院——人工智能公开课06
  18. 导数、偏导数、梯度、方向导数、梯度下降、二阶导数、二阶方向导数
  19. 在线组态工具 html,组态工具-界面组件
  20. 超详细!动态规划详解分析(典型例题分析和对比,附源码)

热门文章

  1. CCS 修改字体大小
  2. 【Hgame2022】第一周misc和web题解
  3. Linux之 prefix 命令
  4. python2.7安装pytorch_Pytorch如何安装,Linux安装Pytorch,OSX安装Pytorch教程
  5. 大数据毕设/课设 - 数据大屏监控可视化设计与实现
  6. 微信小程序自定义card图文组件
  7. 7种常见分布的数学期望及其证明
  8. A Strong Baseline and Batch Normalization Neck for Deep Person Re-identification(论文笔记)(2019CVPR)
  9. android图片压缩不失真实战
  10. BlockingQueue drainTo()