CSS大作业 程序人生

目录

第1章 概述

1.1 Hello简介

根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020
From Program to Process:
Program hello.c经过cpp预处理生成hello.i,cc1编译生成hello.s,as汇编生成hello.o,ld链接生成hello可执行文件,再在shell中键入./hello的命令后,shell fork子进程,execve执行hello程序,此时hello成为Process。
From Zero-0 to Zero-0:
Shell为将要执行hello的子进程execve,设置环境,分配内存等等,之后程序开始载入物理内存,执行可执行文件中的目标代码,CPU为运行的hello分配时间片执行逻辑控制流。程序运行结束后,shell回收hello进程,程序在内存中就不存在了,也就成了zero。

1.2 环境与工具

列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
Core i5-9300h 2.4GHz,Windows,Linux虚拟机
VS2019, VMware16, Ubuntu18.04
Gcc objdump readelf gdb等

1.3 中间结果

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

文件文件作用
hello.i预处理生成的文本文件
hello.s编译之后的汇编文件
hello.o汇编后生成的可重定位目标文件
hello.o.obj.texthello.o的反汇编文件
hello链接之后生成的可执行文件
hello.obj.texthello的反汇编文件

1.4 本章小结

本章主要介绍了hello的p2p,020过程,列出了本次实验信息:环境、中间结果。

第2章 预处理

2.1 预处理的概念与作用

概念:
预处理器cpp根据以字符#开头的命令(宏定义、条件编译),修改原始的C程序,将引用的所有库展开合并成为一个完整的文本文件。
作用:

  1. 将源文件中用#include形式声明的文件复制到新的程序中。hello.c中的#include <stdio.h> #include <unistd.h> #include <stdlib.h> 三个命令告诉预处理器读取系统头文件stdio.h unistd.h stdlib.h 的内容,并把这些文件的内容直接插入到程序文本中。
  2. 用实际值替换用#define定义的符号,#define定义可以分为有参数宏定义和无参数宏定义。
  3. 预处理器根据#if和#ifdef等编译命令及其后的条件,将源程序中的某部分包含进来或排除在外。
  4. 预编译程序可以识别一些特殊的符号。 例如在源程序中出现的__LINE__, __FILE__等符号。

2.2在Ubuntu下预处理的命令

cpp -m64 -no-pie -fno-PIC -E hello.c -o hello.i
截图展示如下
在这里插入图片描述

图2-1预处理命令

2.3 Hello的预处理结果解析

打开hello.i文件,hello.c文件已扩展到3110行,文件的最后一部分如下图所示
在这里插入图片描述

图2-2hello.i中main的位置

main函数被放在文件末尾的位置,预处理后main函数的内容未发生变化,在main之前的内容是对hello.c中的宏的展开的结果,头文件中的内容被包含进该文件中。例如声明函数、定义结构体、定义变量、定义宏等内容。如果有#define定义的内容,预处理会将其替换。

2.4 本章小结

本章介绍了预处理的概念及其所进行的处理,实现将定义的宏进行符号替换、引入头文件的内容、根据指令进行选择性编译等。

第3章 编译

3.1 编译的概念与作用

概念:
编译器将文本文件hello.i翻译成文本文件hello.s,其中的内容为编译后的汇编语言程序,编译就是将高级语言程序转换为汇编语言或者机器语言程序。
下图为编译的流程
在这里插入图片描述

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

3.2 在Ubuntu下编译的命令

命令:gcc -m64 -no-pie -fno-PIC -S hello.i -o hello.s
截图如下
在这里插入图片描述

图3-2编译的命令
## 3.3 Hello的编译结果解析

下图为hello.s的全部内容
在这里插入图片描述
在这里插入图片描述

图3-3hello.s的全部内容

3.3.1 部分汇编指令解释

指令语义
.file源文件名称
.text接下来为代码段
.globl声明一个全局变量
.data接下来为data段
.align声明对齐方式
.type表明该符号所对应的类型
.size声明大小
.long/.string声明一个long/ 声明一个string
.section .rodata接下来为只读数据节

3.3.2 数据

hello.s中包含整数,字符串,数组
整数:

  • int sleepsecs: sleepsecs在hello.c中被声明为全局变量,且已经被赋值(2.5转换成2),编译器处理时在.data节声明该变量,.data节存放已经初始化的全局和静态C变量。在下图中可以看到sleepsecs对齐方式为4,类型为对象,大小为4字节,设置为long类型,值为2

在这里插入图片描述

图3-4sleepsecs的内容
  • int i: i为main函数局部变量,为循环变量。
    下图为循环体的汇编代码,可以看到i被放置在-4(%rbp)的地方
    在这里插入图片描述
图3-5循环的代码
  • int argc; 作为程序的第一个参数,输入,存放在-20(%rbp)的地方
  • 其他数字,如3,10等,直接以立即数的形式出现在汇编代码中
    字符串:
  • “Usage: Hello \345\255\246\345\217\267 \345\247\223\345\220\215\357\274\201”
    该字符串为if中的printf传入的输出格式化参数,可以发现字符串被编码成UTF-8格式,一个汉字在utf-8编码中占三个字节,一个\代表一个字节。
  • “Hello %s %s\n”,该字符串为循环中printf函数所使用的格式匹配字符串。
    两个字符串均存放在只读数据段中。
    数组:
    hello.s中唯一的数组为char* argv[],从循环体代码中可以看出(图3-5),该数组的起始位置为-32(%rbp),其中
    movq -32(%rbp), %rax
    addq $16, %rax
    movq (%rax), %rdx
    获取argv[2]
    movq -32(%rbp), %rax
    addq $8, %rax
    movq (%rax), %rax
    movq %rax, %rsi
    获取argv[1]

3.3.3赋值操作

hello.c中主要的赋值操作包括:

  1. int sleepsecs=2.5; 给全局变量赋初值时,赋值操作不体现在汇编代码中,直接在data段中标明数值。
  2. i=0; 汇编代码中使用movl $0, -4(%rbp)实现。
    在64位程序中,根据赋值的数据类型所占用的空间大小不同,可分为movb,movw,movl,movq。分别对应8字节,16字节,32字节,64字节的数据。

3.3.4类型转换

hello.c中仅有一处隐式类型转换int sleepsecs=2.5,编译器将2.5舍入为2,由double强转为int。

3.3.5 算术操作

hello.c中的算术操作有:
i++ 汇编代码中通过addl $1, -4(%rbp)实现。
其他的算术操作还有

指令效果描述
leaq S, DD ← \leftarrow &S加载有效地址
INC DD ← \leftarrow D+1加1
DEC DD ← \leftarrow D-1减1
NEG DD ← \leftarrow -D取负
NOT DD ← \leftarrow ~D取补
ADD S, DD ← \leftarrow D+S
SUB S, DD ← \leftarrow D-S
IMUL S, DD ← \leftarrow D*S
XOR S, DD ← \leftarrow D^S异或
OR S, DD ← \leftarrow D|S
AND S, DD ← \leftarrow D&S
SAL k, DD ← \leftarrow D<<K左移
SHL k, DD ← \leftarrow D>>K左移(等同于SAL)
SAR k, DD ← \leftarrow D>>AK算术右移
SHR k, DD ← \leftarrow D>>LK逻辑右移

3.3.6 关系操作

hello.c中包含两处关系操作

  1. argc!=3 代码中为cmpl $3, -20(%rbp) je .L2
  2. i<10 代码中为cmpl $9, -4(%rbp) jle .L4

3.3.7 控制转移

程序中涉及的控制转移有:

  1. if (argv!=3):当argv不等于3的时候执行程序段中的代码。对于if判断,编译器使用跳转指令实现,比较argv和3,设置条件码,使用je判断ZF标志位,如果为0,说明argv-3=0也就是argv==3,则不执行if中的代码直接跳转到.L2,否则顺序执行下一条语句,即执行if中的代码。
  2. for(i=0;i<10;i++):使用计数变量i循环10次。汇编代码会无条件跳转到位于循环体.L4之后的比较代码,使用cmpl进行比较,如果i<=9,则跳入.L4 for循环体执行,否则说明循环结束,顺序执行for之后的逻辑。

3.3.8 函数操作

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

hello. c涉及的函数操作有:
main,printf,exit,sleep ,getchar

  1. main:
    参数传递 argc(存在%rdi), argv数组的首地址(存在%rsi)
    函数调用 main函数被程序启动时的代码调用,转到main函数执行
    函数返回 函数正常出口为return 0,将%rax设置为0
  2. printf:
    参数传递 第一次传递的数据为.LC0处的字符串(存在%rdi),第二传递的数据为.LC1处的字符串(存在%rdi),以及argv[1] (存在%rsi),argv[2] (存在%rdx)
    函数调用 第一次printf因为只有一个字符串参数,所以call puts;第二次printf使用call printf。
  3. exit函数:
    参数传递:将%edi设置为1。
    函数调用:call exit。
  4. sleep函数:
    参数传递:将%edi设置为sleepsecs。
    函数调用:call sleep
  5. getchar函数:
    函数调用:call gethcar

3.4 本章小结

本章主要讲述了编译阶段中编译器如何对预处理文件中的各种数据和操作进行处理,并翻译成汇编代码
编译器将.i的预处理文件编译为.s的汇编代码文件。经过编译之后,hello.c从C语言程序转化为更加低级的汇编语言,进而可以进行下一步汇编的操作

第4章 汇编

4.1 汇编的概念与作用

汇编器(as)将汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在.o 目标文件中,.o 文件是一个二进制文件,它包含程序的指令编码。
汇编的作用也就是将汇编指令编写的程序转换为机器语言指令的程序。
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。

4.2 在Ubuntu下汇编的命令

指令as hello.s -o hello.o
在这里插入图片描述

图4-1汇编命令

4.3 可重定位目标elf格式

(1) ELF头:从一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。剩下的部分包含帮助连接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。
在这里插入图片描述

图4-2 ELF头
(2) 节头部表:描述了不同节的位置和大小。目标文件中每个节都有一个固定大小的条目。

在这里插入图片描述

图4-3 节头部表
可以从表中看到.text是可读可执行的,但是不可写。数据段和只读数据段都不可执行,而且只读数据段也不可写。所有段的起始地址都是0 (3) 符号表.symtab:存放程序中定义和引用的函数和全局变量的信息

在这里插入图片描述

图4-4 符号表
(4) 重定位节.rela.text:一个.text 节中位置的列表,包含.text 节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。

在这里插入图片描述

图4-5 重定位节

其中:
Offset(偏移量) 引用的节偏移,表示引用发生的位置
type(类型) 重定位的类型
sym.value + sym.name 符号的信息
addend(加数) 一个有符号常数,一些类型的重定位要使用它对被修改引用的值做偏移调整
最基本的重定位的类型分为两种:R_X86_64_32以及R_X86_64_PC32
R_X86_64_32是重定位绝对引用
R_X86_64_PC32是重定位PC相对引用。
如第一调用printf处两个重定位信息。
在这里插入图片描述

图4-6 第一处printf的重定位信息
地址16处开始的四个字节将会通过重定位绝对引用重定位到.rodata的首地址,也就是前面汇编代码中分析出的第一个字符串的位置,地址1b处开始的四个字节将会通过重定位PC相对引用,重定位到puts-0x4,这样callq指令通过与pc计算就可以得到puts函数执行的首地址 下图为main中其他的重定位信息,可以看到基本上是同样的思路,而且字符串使用的是绝对引用,全局变量,函数调用使用的是相对引用

在这里插入图片描述

图4-6 其它的重定位信息

4.4 Hello.o的结果解析

objdump -d -r hello.o 分析hello.o的反汇编,并与第3章的 hello.s进行对照分析。
执行命令objdump hello.o >hello.o.obj.text -d -r,得到的文件如下。
在这里插入图片描述

图4-8 hello.o中的机器指令与汇编代码

下图为hello.s中的main函数汇编代码。
在这里插入图片描述

图4-9 hello.s中的汇编代码

机器语言程序的是二进制机器指令的集合,机器指令由操作码和操作数构成
每一条汇编指令都有对应的操作码和操作数,进而可以将所有的汇编指令和二进制机器指令建立一一映射的关系,因此可以将汇编语言转化为机器语言。
hello.s与hello.o中的不同之处:
(1) 分支转移:反汇编的跳转指令用的不是段名称比如.L3,而是用的确定的地址。

在这里插入图片描述

图4-10 hello.s中的第一处跳转

在这里插入图片描述

图4-11 hello.o中的第一处跳转

(2)函数调用:在.s文件中,callq后为函数名称,而在.o中,call的目标地址是当前下一条指令。这是因为 hello.c 中调用的函数都是外部函数,在汇编成为可重定位文件的时候,函数的地址仍未确定,将其call指令后的相对地址设置为全0(目标地址正是下一条指令),然后在.rela.text 节中为其添加重定位条目,在链接的时候再修改

4.5 本章小结

本章对hello.s进行汇编,生成了hello.o可重定位目标文件,并且分析了可重定位文件的ELF头、节头部表、符号表和可重定位节,比较了hello.s和hello.o反汇编代码的不同之处。

第5章 链接

5.1 链接的概念与作用

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

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 hello.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o -o hello
在这里插入图片描述

图5-1链接指令

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

分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
(1) ELF头:hello的文件头和hello.o文件头的不同之处有文件的类型从可重定位变为可执行文件,节头数量增加,程序的入口点地址也已确定

在这里插入图片描述

图5-2 ELF头

(2) 节头部表: hello可执行文件中每个节的起始地址已经由全部为0修改载入内存时的地址
在这里插入图片描述

图5-3 节头部表

(3) 重定位节.rela.text 该节在hello中仍然存在,是由于重定位为动态链接,需要在程序加载和运行时完成
在这里插入图片描述

图5-4 重定位节

(4) 符号表
在这里插入图片描述

图5-5 符号表(部分)

5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
在这里插入图片描述

图5-6 edb中hello的起始地址

可以看到,hello的起始地址为0x400000,根据5.3节中的节头部表,可以找到各个节的信息,
如.text节,
在这里插入图片描述

图5-7 节头部表部分内容

起始地址为400500
在这里插入图片描述

图5-8 edb中对应地址的内容

再比如.rodata节,图5-7中表明起始地址为400630
在这里插入图片描述

图5-9 edb中对应地址的内容

可以看到内容为源程序中定义的字符串
5.5 链接的重定位过程分析
objdump -r -d hello > hello.obj.text
在这里插入图片描述

图5-10 hello反汇编内容中的main节

hello反汇编的代码中多了很多的节以及很多函数的汇编代码
下面分析hello与hello.o的不同
(1) hello反汇编的代码有确定的虚拟地址,也就是说已经完成了重定位,
如main的地址为400532。
而hello.o反汇编代码中代码的虚拟地址均为0,未完成重定位的过程。
(2) hello反汇编的代码中增加了大量的节与大量的函数

在这里插入图片描述在这里插入图片描述
在这里插入图片描述

图5-11 hello反汇编内容新增的内容

新增的内容

  1. .init节 程序初始化所需要执行的代码
  2. .plt节 动态链接-过程链接表
  3. .fini节 当程序正常终止时需要执行的代码
    在.text节中还有几个新出现的函数如<_start>,<_dl_relocate_static_pie>,<__libc_csu_init>,<__libc_csu_fini>。

hello的反汇编代码中可以看到,连接时已经完成了部分重定位,
首先是main函数地址确定为0x400532,以及对数据的引用地址也已确定
在这里插入图片描述

图5-12 hello第一个printf的参数地址

如第一处printf的参数,已确定参数地址为0x400634
在这里插入图片描述

图5-12 hello sleep的参数地址

此处的sleepsecs的地址也已确定,为0x601044
可以看到由于使用的为pc相对地址引用, 0x601044-0x400592=0x200ab2符合
此时hello还有部分需要重定位的信息,如下图展示
在这里插入图片描述

图5-12 hello 重定位

这些信息的链接在程序加载的时候进行,例如puts
在这里插入图片描述

图5-12 hello中puts的反汇编代码

puts执行会先跳转到全局偏移量表GOT中对应地址(此处puts对应0x601018)中存放的地址来执行,此时GOT表中的内容为
在这里插入图片描述

图5-12 GOT中0x601018的内容

该值为puts@plt中第二条指令,需要在此处压入需要跳转的地址。真正执行时的地址需要在程序加载的时候由链接器写入。

5.6 hello的执行流程

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

  • 执行 ./edb –run ./hello
ld -2.27.so!_dl_start00007efe8730fea0
ld-2.27.so!_dl_init00007efe8731e7d0
hello!_start0000000000400500
libc-2.27.so!__libc_start_main00007f520cc68b10
hello!puts@plt00000000004004b0
hello!exit@plt00000000004004e0
  • 执行 ./edb –run ./hello 1190202209 俞磊
ld -2.27.so!_dl_start00007f81da08eea0
ld-2.27.so!_dl_init00007f81da09d7d0
hello!_start0000000000400500
libc-2.27.so!__libc_start_main00007f81d9cbdb10
hello!printf@plt(调用了10次)00000000004004c0
hello!sleep@plt(调用了10次)00000000004004f0
hello!getchar@plt00000000004004f0
libc-2.27.so!exit00007f5784c59240

5.7 Hello的动态链接分析

分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
对于动态共享链接库中PIC函数,编译器没有办法预测函数的运行时地址,所以需要为其添加重定位记录,并等待动态链接器处理。为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表PLT和全局偏移量表GOT实现函数的动态链接。其中GOT 中存放函数目标地址,PLT使用 GO T中地址跳转到目标函数。
分析hello中的动态链接
由hello反汇编代码得到
在这里插入图片描述

图5-13 .plt节的内容

GOT起始地址为0x601008
dl_init前:
在这里插入图片描述

图5-12 GOT表,修改前

dl_init后:
在这里插入图片描述

图5-12 GOT表,修改后

因此
GOT[1]为 00 00 7f dd 2b ff 31 70 指向重定位表,依次为.plt节需要重定位的函数的运行时地址
GOT[2]为 00 00 7f dd 2b dd f8 20 指向动态链接器ld-linux.so运行时地址。
当程序执行到printf等函数时,原先代码中需要push的值已被填上
在这里插入图片描述

图5-12 动态链接后的内容

5.8 本章小结

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

第6章 hello进程管理

6.1 进程的概念与作用

概念: 进程是一个执行中的程序的实例。系统中的每个程序都运行在某个进程的上下文中。
作用:进程提供给应用程序两个关键抽象

  1. 一个独立的逻辑控制流:它提供一个假象,好像我们的程序独占地使用处理器。
  2. 一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用内存系统。

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

shell是一种交互型应用程序,它代表用户运行其他程序。shell执行一系列的读/求值步骤,然后终止。读步骤读取来自用户的一个命令行;求值步骤解析命令行,并代表用户运行程序。shell还应该能对前台/后台的进程进行管理(比如回收僵死进程),并能对用户发送的信号做出合理的反应。
处理流程:

  1. 将用户输入的命令行进行解析,分析是否是内置命令;
  2. 若是内置命令,直接执行;若不是内置命令,则bash在初始子进程的上下文中加载和运行它。
  3. 本质上就是shell在执行一系列的读和求值的步骤,在这个过程中,他同时可以接受来自终端的命令输入。

6.3 Hello的fork进程创建过程

在终端键入 ./hello 1190202209 俞磊后,bash分析该命令不是内置命令,于是调用fork()函数,就创建了一个子进程。其函数原型为pid_t fork(void);对于返回值,若成功调用一次则返回两个值,子进程返回0,父进程返回子进程ID;否则,出错返回-1。创建的子进程得到与父进程的用户级虚拟地址空间相同的一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程有与父进程最大区别为具有不同的PID。此时bash会等待前台fork出来的子进程结束运行

6.4 Hello的execve过程

execve函数在当前进程的上下文中加载并运行一个新的程序,此处即为hello。
函数原型为
int execve(const char *filename, const char *argv[], const char *envp[]);
如果加载成功,则不返回,只有当出现错误时,才会返回。
execve会覆盖当前进程的代码、数据、栈,但是会保留PID以及继承已打开的文件描述符。
新程序启动后的栈结构如下
在这里插入图片描述

图6-1 exceve加载新程序启动后的栈结构

6.5 Hello的进程执行

  1. 逻辑控制流:一系列程序计数器PC的值的序列叫做逻辑控制流,进程是轮流使用处理器的,在同一个处理器核心中,每个进程执行它的流的一部分后被抢占(暂时挂起),然后轮到其他进程。
  2. 时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
  3. 用户模式和内核模式:处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
  4. 调度:在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被强占的进程。这种决策就叫调度(是由内核中的调度器的代码处理的)。
  5. 上下文切换:在内核调度了一个新的进程运行时,它就抢占当前进程,并使用一种上下文切换的机制来控制转移到新的进程。
    1) 保存当前进程的上下文
    2) 恢复某个先前被强占的进程被保存的上下文
    3) 将控制传递给这个新恢复的进程
    上下文: 上下文就是内核重新启动一个被抢占的进程所需要的状态,它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表,包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。
  6. 用户态核心态转换
    进程hello初始运行在用户模式中,直到它通过执行系统调用函数sleep或者exit时便陷入到内核。内核中的处理程序完成对系统函数的调用。之后,执行上下文切换,将控制返回给进程hello系统调用之后的那条语句。
    如sleep调用过程:
    在这里插入图片描述
图6-2进程切换

6.6 hello的异常与信号处理

异常可以分为以下四类:

类型原因异步/同步返回行为
中断来自I/O设备的信号异步总是返回到下一条指令
陷阱有意的异常同步总是返回到下一条指令
故障潜在可恢复的错误同步可能会返回到当前指令
终止不可恢复的错误同步不会返回

正常运行hello:
在这里插入图片描述

图6-3正常运行

hello运行时进行输入:
在这里插入图片描述

图6-4进行输入

可以看到除了第一次敲下回车时被作为hello的输入接受以外,其他的输入会以回车作为分隔逐个被bash认为是命令来执行
hello运行时键入Ctrl+c以及Ctrl+z
在这里插入图片描述

图6-5键盘信号

在程序运行时键入Ctrl+z,hello程序会被挂起,停止运行,通过jobs指令可以看到该作业的序号为1。通过ps指令可以看到进程仍然存在。
此时通过fg指令,向hello程序发送SIGCONT信号,使得进程继续运行。使得hello程序回到前台继续执行,shell程序首先打印hello的命令行命令,hello继续运行。
接着键入Ctrl+c,hello程序被终止。进程被回收
在这里插入图片描述

图6-6进程已被回收

键入Ctrl+z:shell收到SIGSTP信号,并转发给hello程序进入后台使之挂起。
键入Ctrl+c:shell收到SIGINT信号,并转发给hello程序使之终止,接着shell会回收hello进程。

6.7本章小结

在本章中,阐述了进程的概念与作用,同时介绍了 Shell 的一般处理流程和作用,并且着重分析了调用 fork 创建新进程,调用 execve函数执行hello的过程,hello的进程执行,以及hello 的异常与信号处理。

第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:
在有地址变换功能的计算机中,访问指令给出的地址 (操作数) 叫逻辑地址,也叫相对地址。要经过寻址方式的计算或变换才得到内存储器中的物理地址。这样该存储单元的地址就可以用段基址(段地址)和段内偏移量(偏移地址)来表示,段基址确定它所在的段居于整个存储空间的位置,偏移量确定它在段内的位置,这种地址表示方式称为逻辑地址,通常表示为段地址:偏移地址的形式。
线性地址:
是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。
虚拟地址:
CPU启动保护模式后,程序运行在虚拟地址空间中。注意,并不是所有的“程序”都是运行在虚拟地址中。CPU在启动的时候是运行在实模式的,Bootloader以及内核在初始化页表之前并不使用虚拟地址,而是直接使用物理地址的。在本课程内容中,虚拟地址就是线性地址
物理地址:
放在寻址总线上的地址。放在寻址总线上,如果是读,电路根据这个地址每位的值就将相应地址的物理内存中的数据放到数据总线中传输。如果是写,电路根据这个地址每位的值就在相应地址的物理内存中放入数据总线上的内容。物理地址是地址变换的最终结果地址。

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

一个逻辑地址由两部份组成,段标识符和段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。如图所示
在这里插入图片描述

图7-1 段选择符

索引号,可以理解为数组的下标——而它将会对应段描述符数组,段描述符具体地址描述了一个段这样,很多个段描述符,就组了一个数组,叫“段描述符表”,这样,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,每一个段描述符由8个字节组成,如下图
在这里插入图片描述

图7-2 段描述符

Base字段,表示的是包含段的首字节的线性地址,也就是一个段的开始位置的线性地址。一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。通过段选择符中的TI字段来表示处于GDT还是LDT中,=0,表示用GDT,=1表示用LDT,GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。
下图展示了如何将一个逻辑地址转换成相应线性地址
在这里插入图片描述

图7-3 转换过程
  1. 首先给定一个完整的逻辑地址[段选择符:段内偏移地址],
  2. 看段选择符的T1=0还是1,即先检查段选择符中的TI字段,以决定段描述符保存在哪一个描述符表中,知道当前要转换是GDT中的段(在这种情况下,分段单元从gdtr寄存器中得到GDT的线性基地址),还是LDT中的段(在这种情况下,分段单元从ldtr寄存器中得到GDT的线性基地址),再根据相应寄存器,得到其地址和大小。
  3. 拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,确定了Base,即基地址就知道了。
  4. 把Base + offset,就是要转换的线性地址了

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

hello程序中,线性地址就是虚拟地址,因此,要进行的就是虚拟地址到物理地址的变换,内存管理单元(MMU)利用页表来实现地址翻译,即从虚拟地址空间到物理地址空间的映射。CPU中的一个控制寄存器,页表基址寄存器(PTBR)指向当前页表。n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移(VPO)和一个(n-p)位的虚拟页号(VPN)。MMU利用VPN选择合适的页表条目(PTE)。将PTE中的物理页号(PPN)和虚拟地址中的VPO串联起来,就得到相应的物理地址。
在不考虑多级页表与TLB的情况下,翻译过程如下图所示
在这里插入图片描述

图7-4 VA到PA的转换

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

TLB:翻译后备缓冲器,是MMU中一个小的具有高相联度的集合,帮助实现虚拟页号向物理页号的映射,页数很少的页表可以完全放在TLB中。MMU利用TLB查找PTE的过程如下图所示
在这里插入图片描述

图7-5利用TLB寻址

Core i7使用四级页表来将VA翻译成PA。示意图如下
在这里插入图片描述

图7-6 Core i7翻译流程

接下来对上述过程进行分析
CPU产生虚拟地址VA,VA传送给MMU,MMU使用前36位VPN作为TLBT(前32位)+TLBI(后4位)在TLB中匹配,如果命中,则得到PPN(40bit)与VPO(12bit)组合成PA(52bit)。
如果TLB中没有命中,MMU向页表中查询,CR3确定第一级页表的起始地址,VPN1(9bit)确定在第一级页表中的偏移量,查询出PTE,如果在物理内存中且权限符合,确定第二级页表的起始地址,以此类推,最终在第四级页表中查询到PPN,与VPO组合成PA。

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

地址翻译完成后,CPU在高速缓存中进行匹配,匹配过程如下图所示
在这里插入图片描述

图7-7 缓存内存访问

该图实际上为图7-6右半部分。PA被分为三个部分CT CI CO。CI确定组索引,CT在每一个组内匹配,CO为块内偏移量。
过程为:使用CI进行组索引匹配,每组8路,对8路的块分别匹配CT,如果匹配成功且块的valid标志位为1,则命中(hit),根据数据偏移量CO取出数据返回。如果没有匹配成功或者匹配成功但是标志位是0,则不命中(miss),向下一级缓存中查询数据(L2 Cache->L3 Cache->主存)。

7.6 hello进程fork时的内存映射

虚拟内存和内存映射解释了fork函数如何为每个新进
程提供私有的虚拟地址空间。

  1. 为新进程创建虚拟内存
  2. 创建当前进程的mm_struct、vm_area_struct和页表的原样副本。
    两个进程中的每个页面都标记为只读。
    两个进程中的每个区域结构(vm_area_struct)都标记为私有的写时复制
  3. 在新进程中返回时,新进程拥有与调用fork进程相同的虚拟内存
  4. 随后的写操作通过写时复制机制创建新页面

7.7 hello进程execve时的内存映射

execve函数在当前进程中加载并运行新程序hello的步骤:

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

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

缺页故障是一种常见的故障,当指令引用一个虚拟地址,在MMU中查找页表时发现与该地址相对应的物理地址不在内存中,因此必须从磁盘中取出的时候就会发生故障。其处理流程遵循图所示的故障处理流程。
在这里插入图片描述

图7-9故障处理流程

缺页处理程序选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入需要加载的页面并更新页表。然后将控制返回给引起缺页故障的指令。此时MMU可以正常得到PA。

7.9动态存储分配管理

动态内存分配器维护一个进程的虚拟内存区域称为堆。分配器将堆视为一组不同大小的块(block)的集合。
在这里插入图片描述

图7-10 堆

每个块要么是已分配的,要么是空闲的。已分配的块被释放后变为空闲块。根据这种释放是否是由应用程序显示执行的,内存分配器被分为显示分配器和隐式分配器。
显示分配器要求应用显示地释放任何已分配的块。C标准库提供的malloc程序包就是显示分配器。
隐式分配器会检测到一个已分配块不再被程序所使用,就会释放这个块。隐式分配器也叫垃圾收集器,自动释放未使用的已分配块的过程叫垃圾收集。
显示分配器有以下限制条件

  • 可以处理任意的分配( malloc)和释放(free)请求序列
  • 只能释放已分配的块
  • 无法控制分配块的数量或大小
  • 立即响应 malloc 请求
  • 不允许分配器重新排列或者缓冲请求
  • 必须从空闲内存中分配块
  • 必须对齐块,使它们可以保存任何类型的数据对象
  • 只能操作或改变空闲块
  • 一旦块被分配,就不允许修改或移动它
    隐式空闲链表
    块结构如下:
    在这里插入图片描述
图7-11隐式空闲链表的块结构

一个块是由一个字的头部、有效载荷,以及可能的填充组成。头部编码了这个块的大小(包括头部和所有的填充),以及这个块是已分配的还是空闲的。块的头最后一位指明这个块是已分配的还是空闲的。头部后面是应用malloc时请求的有效载荷。有效载荷后面是一片不使用的填充块,其大小可以是任意的。空闲块通过头部块的大小字段隐含的连接着,所以称这种结构就隐式空闲链表。
在这里插入图片描述

图7-12 隐式空闲链表示意图

这种隐式链表在合并空闲块时性能较差,有一种带边界标记的隐式空闲链表。链表结构以及块结构如下图所示
在这里插入图片描述

图7-13 带边界标记的隐式空闲链表

这种模式的空闲块合并可以在常数时间内完成

显示空闲链表
显示空闲链表是将空闲块组织为某种形式的显示数据结构。
在这里插入图片描述

图7-14 显式空闲链表的块结构

堆被组织为一个双向空闲链表,在每个空闲块中,都包含一个前驱和后继的指针。使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。
一种方法使用后进先出的顺序维护链表,将新释放的块在链表的开始处。使用LIFO的顺序和首次适配的放置策略,分配器会最先检查最近使用过的块,在这种情况下,释放一个块可以在线性的时间内完成,如果使用了边界标记,那么合并也可以在常数时间内完成。
按照地址顺序来维护链表,其中链表中的每个块的地址都小于它的后继的地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。平衡点在于,按照地址排序首次适配比 LIFO 排序的首次适配有着更高的内存利用率,接近最佳适配的利用率。

7.10本章小结

本章主要介绍了hello的存储器的地址空间,介绍了四种地址空间的概念和从逻辑地址到线性地址再到物理地址的相互转换。同时介绍了利用四级页表与TLB的虚拟地址空间到物理地址的转换。阐述了三级cache的物理内存访问、进程 fork 时的内存映射、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化
一个 Linux 文件 就是一个 m 字节的序列:B0 , B1 , … , Bk, … , Bm-1
所有的I/O设备(网络、磁盘、终端)都被模型化为文件例如:

  • /dev/sda2文件是用户磁盘分区,
  • /dev/tty2文件是终端

甚至内核也被映射为文件如

  • /boot/vmlinuz-3.13.0-55-generic文件是内核映像,
  • /proc文件是内核数据结构。
    设备管理:unix io接口
    这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O:
  • 打开关闭操作open()和close();
  • 读写操作read()和write();
  • 改变当前文件位置(seek)等

每个Linux文件都有一个类型(type)来表明它在系统中的角色:

  • 普通文件 (Regular file): 包含任意数据
  • 目录 (Directory): 包含一组链接的文件,每个链接都将一个文件名映射到一个文件/
  • 套接字 (Socket): 用来与另一个进程进行跨网络通信的文件
  • 命名通道(Named pipes(FIFOs))、符号链接(Symbolic links)、字符和块设备(Character and block devices)等

8.2 简述Unix IO接口及其函数

  1. 打开文件:通知内核准备好访问该文件
    int open(char *filename, int flags, mode_t mode);
    返回一个小的描述符数字---- 文件描述符。返回的描述符总是在进程中当前没有打开的最小描述符。返回-1说明发生错误
    flags表明访问的该文件的模式。如O_RDONLY(只读模式)、O_WRONLY (只写模式)、O_RDWR(读写模式)
    mode 创建文件时使用,用其指定新文件的访问权限位
  2. 关闭文件:通知内核不再访问文件,内核释放文件打开时创建的结构体,内核将描述符释放给可用描述符池,下次打开某个文件时,从该池中分配一个最小可用的描述符
    int close(int fd) ;
  3. 读文件:从当前文件位置复制字节到内存位置,然后更新文件位置
    ssize_t read(int fd,void *buf,size_t n);
    返回值表示的是实际传送的字节数量。返回类型 ssize_t 是有符号整数。返回值 < 0 表示发生错误, 返回值< sizeof(buf)是可能的,复制的为不足值。
  4. 写文件:从内存复制字节到当前文件位置,然后更新文件位置
    ssize_t wirte(int fd,const void *buf,size_t n),write 函数从内存位置 buf 复制至多 n 个字节到描述符为 fd 的当前文件位置。
    返回值表示的是从内存向文件fd实际传送的字节数量。返回值< 0 表明发生错误 ,同读文件一样, 不足值(short counts) 是可能的

8.3 printf的实现分析

分析首先查看printf函数的函数体:

int printf(const char *fmt, ...){
int i;
char buf[256];
   
     va_list arg = (va_list)((char*)(&fmt) + 4);
     i = vsprintf(buf, fmt, arg);
     write(buf, i);
   
     return i;
}

printf程序向va_list arg数据结构中传递该次调用printf的第二个参数,即(char * )(&fmt+4);
再看vsprintf函数

int vsprintf(char *buf, const char *fmt, va_list args) 
   { 
    char* p; 
    char tmp[256]; 
    va_list p_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); 
   }

根据printf中的i = vsprintf(buf, fmt, arg),可以判断出返回值为打印出来的字符串的长度,所以vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
接着通过write函数输出:

write:
     mov eax, _NR_write
     mov ebx, [esp + 4]
     mov ecx, [esp + 8]
     int INT_VECTOR_SYS_CALL

这里是给几个寄存器传递了几个参数,然后一个int结束int INT_VECTOR_SYS_CALL表示要通过系统来调用sys_call这个函数。

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

8.4 getchar的实现分析

getchar函数体:

int getchar(void)
{
    static char buf[BUFSIZ];
    static char* bb=buf;
    static int n=0;
    if(n==0)
    {
        n=read(0,buf,BUFSIZ);
        bb=buf;
    }
    return (--n>=0)?(unsigned char)*bb++:EOF;
}

运行到getchar函数时,程序将控制权交给内核。当你键入时,内容进入缓冲区并在屏幕上回显。按下回车,通知内核输入完成,这时再将控制权交还给程序。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

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

结论

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

hello.c:编写c程序,它是一个二进制文本文件,hello.c中的每个字符都是用ascall编码表示。
hello.i:hello.c经过预处理阶段变为hello.i。
hello.s:hello.i经过编译阶段变为hello.s。
hello.o:hello.s经过汇编阶段变为hello.o。
hello:hello.o与可重定位目标文件和动态链接库链接成为可执行文件hello。至此可执行hello程序正式诞生。
运行:在终端输入./hello 1190202209 俞磊。hello启动运行
创建子进程:由于终端输入的不是一个内置的shell命令,因此shell调用fork()函数创建一个子进程。
加载:shell 调用 execve,execve 调用启动加载器,映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入 main 函数。
上下文切换:hello调用sleep函数之后进程陷入内核模式,处理休眠请求主动释放当前进程,内核进行上下文切换将当前进程的控制权交给其他进程,当sleep函数调用完成时,内核执行上下文切换将控制传递给当前进程。
动态申请内存:当hello程序执行printf函数,会调用 malloc 向动态内存分配器申请堆中的内存。
信号管理:当程序在运行的时候我们输入Ctrl+c,内核会发送SIGINT信号给进程并终止前台作业。当输入Ctrl+z时,内核会发送SIGTSTP信号给进程,并将前台作业停止挂起。
终止:当子进程执行完成时,内核安排父进程回收子进程,将子进程的退出状态传递给父进程。内核删除为这个进程创建的所有数据结构。
任何一个复杂或简单的程序都要在计算机中经历如此复杂的流程,才能最终完成这个程序的任务,在这个过程中,计算机各部分需要高度配合才能完成。每个部分的设计后面都是前人的努力的结果
(结论0分,缺失 -1分,根据内容酌情加分)

附件

列出所有的中间产物的文件名,并予以说明起作用。
文件名 文件作用
hello.i 预处理后的文件
hello.s 编译后的汇编文件
hello.o 汇编之后的可重定位目标文件
hello.o.obj.text hello.o 的反汇编文件
hello.o 可执行文件
hello.obj.text hello的反汇编文件

参考文献

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值