HITCS大作业论文——Hello的一生

HITCS大作业论文——Hello的一生

摘 要

本文主要通过计算机系统的相关知识,描述了Hello的一生所经历的过程:
预处理,将hello.c所有的头文件的相关代码合并成一个hello.i文件。编译,将hello.i或hello.c文件编译成汇编文件hello.s, c语言变成汇编语言。汇编,将hello.s翻译成可重定位目标文件hello.o, 汇编语言加上机器代码。链接,将hello.o与库函数连接成可执行目标程序hello。Shell运行,在Shell中输入运行指令./hello
1180300601 马铖君。首先Shell判断它不是内部指令,于是fork了一个子进程调用execve函数运行hello程序。成为进程,Hello成为进程之后,也许会接受到很多信号,比如说ctrl+z和ctrl+c分别表示挂起(SIGSTP)和终止(SIGINT)的信号。内核会根据不同的信号,对hello这个进程进行相应的逻辑控制流。访存,内存管理单元(MMU)将逻辑地址转变为虚拟内存地址、再由虚拟内存地址转换为物理内存地址,从物理内存地址一步步向上一级储存结构进行寻找相关数据。直到找到相关的数据。申请动态内存,printf语句会调用malloc想动态内存分配器申请堆中的内存。终止与回收,最终所有进程的父进程来回收这个子进程,内核中将删除这个进程创建的所有数据,Hello的一生到此终止。
这10个步骤将hello程序与计算机系统紧紧地联系在了一起,让我们了解到
了计算机系统的原理

目 录

第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简介

Hello的一生所经历的过程:第一步,预处理,将hello.c所有的头文件的相关代码合并成一个hello.i文件。第二步,编译,将hello.i或hello.c文件编译成汇编文件hello.s, c语言变成汇编语言.。第三步,汇编,将hello.s翻译成可重定位目标文件hello.o, 汇编语言加上机器代码。第四步,链接,将hello.o与库函数连接成可执行目标程序hello。第五步,Shell运行,在Shell中输入运行指令./hello 1180300601 马铖君。首先Shell判断它不是内部指令,于是fork了一个子进程调用execve函数运行hello程序。Hello成为进程之后,也许会接受到很多信号,比如说ctrl+z和ctrl+c分别表示挂起(SIGSTP)和终止(SIGINT)的信号。内核会根据不同的信号,对hello这个进程进行相应的逻辑控制流。最后一步,所有进程的父进程来回收这个子进程,内核中将删除这个进程创建的所有数据,Hello的一生到此终止。

1.2 环境与工具

1.2.1 硬件环境

CPU:Intel® Core™
i7-6700HQ @ 2.50GHz (64位)

GPU:Intel® HD
Graphics 620

  Nvidia GeForce 940MX

物理内存:12.00GB

磁盘:500GB SSD

  2TB

HDD

1.2.2 软件环境

Windows10 64位;

Vmware Workstation Pro;

Ubuntu 18.04 64位;

1.2.3工具

gedit,gcc,readelf, objdump,
hexedit, edb

1.3 中间结果

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

hello.c:hello程序c语言源文件

hello.i:hello.c预处理生成的文本文件

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

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

hello.elf:使用readelf读取hello.o得到的信息

hello.o.asm:使用objdump反汇编hello.o得到的反汇编代码

hello:由hello.o与动态链接库链接得到的可执行目标文件

Hello.elf:使用readelf读取hello得到的信息(这个是大写H的命名)

hello.asm:使用objdump反汇编hello得到的反汇编代码

1.4 本章小结

本章简述了hello的一生所经历的事情:预处理、编译、汇编、链接、成为进程与信号相作用、最终终止释放掉储存空间。

第2章 预处理

2.1 预处理的概念与作用

预处理是将引用的头文件的代码加入到C语言代码中,并用实际值替换define定义的字符串,为的是能将hello更好地翻译成汇编语言。

2.2在Ubuntu下预处理的命令

输入gcc hello.c -E -o hello.i,将C语言源程序文件进行预处理

2.3 Hello的预处理结果解析

用code:Blocks打开对文件进行查看

程序的预处理过程是将头文件的内容添加到程序里,所以会由原来的十几行变为三千多行。其中,hello.c的头文件包括<stdio.h><unistd.h><stdlib.h>

2.4 本章小结

对于程序的预处理,Ubuntu只是把头文件的内容添加到了hello.c里,以至于在hello.i中我们能找到stdio.h、unistd.h等的语句。C语言代码并没有发生改变,并保存在最后。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

编译是由编译器将hello.i预处理文件翻译成hello.s编写文件的过程。其中,hello.s记录了c程序对应的汇编代码,从而便于机器识别,为下一步转换为机器代码做好条件。

3.2 在Ubuntu下编译的命令

输入gcc -S hello.c -o hello.s

编译过程如下:

3.3 Hello的编译结果解析(3.3.1~3.3.4)

3.3.1 汇编器和链接器声明数据类型

所有以“.”开头的行都是指导汇编器和链接器的命令。通常这些代码记录了hello.c程序文件的规格性质以及声明的变量,例如使用的字长、使用的特殊字符串等。

在图中,我们可以找到.text段,它用来存放已编译程序的机器代码;

.data段用来存放全局变量sleepsecs以及主函数;

.rodata段只用来读取数据,在这里它读取了printf()语句中的“Usage:
Hello \345\255\246\245\217\267\ 345\247\223\243\220\215\257\274\201”和“Hello %s %s\n”,其中\345\225\246\等对应了汉字的UTF-8的编码表示。

所以,.LC0的字符串为“Usage: Hello 学号 姓名!\n”

这些由“.”作为开头的命令,让汇编器和链接器了解到了这个函数对各种变量的声明(这些变量包括全局变量sleepsecs、局部变量、以及函数声明),即对符号的声明。

3.3.2 分析主函数汇编代码

return 0;

















输出“Hello %s %s\n”

















sleep(sleepsecs);

















for(i=0;i<10;i++)

















这里与3比较,所以%edi中一定储存变量argc,并将其存在栈-20(%rbp)中

















输出“Usage: Hello 学号 姓名!\n”

















#define register 16

















#define offset 16

3.3.3分析数据类型

通过读汇编代码并与程序作比对,我深刻理解了这些汇编代码。进而能对数据类型有更深的理解。

  1. 首先是argc:

来自于:int main(int argc,char *argv[]),是一个int类型形参

argc储存在寄存器%rdi中,由cmpl $3, -20(%rbp)对应if(argc!=3)语句而得知。

movl %edi,
-20(%rbp)将argc储存在栈中。

2.第二是字符串argc[]:

来自于:int main(int argc,char *argv[]),是一个char *类型形参

argv[]储存在寄存器%rsi中,movl %edi, -32(%rbp)将argv[0]储存在栈中。

语句段movq-32(%rbp), %rax

addq $16, %rax

movq (%rax), %rdx

movq -32(%rbp), %rax

addq $8, %rax

movq (%rax), %rax

movq %rax, %rsi分别对应将argv[1]和argv[2]通过%rax存放到寄存器%rdx和%rsi中。

3.第三是sleepsecs:

来自于int sleepsecs=2.5;,是一个int类型全局变量

在文件开头声明全局变量数据类型时,编译器在.data段声明了变量,并设置为4字节,long类型,值为2,原来定义的2.5变成2是因为进行了向0取整。

4.第四是i:

来自于for(i=0;i<10;i++),是一个int类型局部变量

语句movq $0, -4(%rbp)

jmp.L3

.L4:

movq (%rax), %rax

addl $1, -4(%rbp)

.L3:

cmpl $9, -4(%rbp)

jle.L4

对应了for(i=0;i<10;i++)循环。变量i储存在栈中-4(%rbp)。

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

5.第五是printf输出的字符串

来自于printf(“Usage: Hello 学号 姓名!\n”); 与printf(“Hello
%s %s\n”, argv[1],argv[2]);

其中“Usage:
Hello 学号 姓名!\n”“ Hello %s
%s\n”内容分别储存在.LC0与.LC1中。对应语句为:

.LC0:

  .string    "Usage: Hello \345\255\246\345\217\267

\345\247\223\345\220\215\ 357\274\201"

.LC1:

  .string    "Hello %s %s\n"

在汇编代码中,分别以这样的方式输出:

leaq .LC0(%rip),
%rdi

leaq .LC1(%rip), %rdi

3.3.4 详细操作总结

汇编语言的详细操作有:

  1. 数字表示:

常量 $1

变量 储存在%rsi中

类型 .long 为长整形

  1. 数据传输指令

  2. 算数操作

++ addl $1, %rax

  1. 关系操作

指令

操作

描述

CMP S2,S1

S1-S2

比较

TEST S2,S1

S1&S2

测试

执行完指令后依据结果修改条件码,形成关系操作。

如cmpl $9,
-4(%rbp)判断i比9大就跳出循环。

   jle   .L4
  1. 数组/指针/结构操作

Argv[i] movq-32(%rbp), %rax 将Argv[1]存入%rdx

addq $16, %rax

movq (%rax), %rdx

3.4 本章小结

操作详细总结是根据书上知识,把部分语句挑出来当示例。

本章是重要的一节,将高级语言变为了汇编语言,对转变成机器代码走了重要一步。通过查看hello.s,对机器的工作流程有了初步了解。

第4章 汇编

4.1 汇编的概念与作用

汇编器(as)将.s汇编语言翻译成为机器语言,在这个过程中,也可以将C程序中应用的库函数一起汇编,最终形成机器可识别的并运行hello.o可执行文件。

4.2 在Ubuntu下汇编的命令

输入:gcc -m64 -no-pie -fno-PIC -c
hello.s -o hello.o

4.3 可重定位目标elf格式

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

输入:readelf -a hello.o > hello.elf

打开hello.elf

4.3.1 总括

ELF可重定位目标文件包含下面几个节

4.3.2 ELF头

ELF头描述了字节顺序(小端序)、ELF头的大小56字节、系统架构X86-64、节头部表的偏移等信息。

4.3.3节头

节头记录了每个节的名称、偏移量、大小、位置等信息。

4.3.4重定位节

重定位节存放着代码的重定位条目。当链接器把这个目标文件和其他文件组

合时,会结合这个节,修改.text节中相应位置的信息。

重定位节的计算步骤:

refptr = s +r.offset

refaddr = ADDR(s) + r.offset

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

其中ADDR(s).text或.data的地址

r.offset为偏移量

r.addend包括symbol 和type 两部分,其中symbol 占前4 个字节, type 占后4 个字

节,symbol 代表重定位到的目标在.symtab中的偏移量,type 代表重定位的类型

4.3.5 符号表

符号表储存程序中定义和引用的函数和变量(全局/外部/局部变量)的信息。

4.4 Hello.o的结果解析

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

1.分析hello.o.asm

(1)文件由代码语句地址、机器语言、汇编语言组成。

其中,机器语言由操作码、操作数的地址、操作结果的储存地址组成。

(2)每一个操作指令映射着一个机器代码,例如:jmpq的操作码就为e9;在引用立即数时,他们也会以16进制进行储存,例如0x123456会以小端法储存为56
34 12;

(3)某些最小端储存了call指令的相对地址,当前地址加上这个值之后就会跳转到相应代码地址。这就是分支转移,例如:

73e +(0xffffffd0)(有符号数)= 650

  1. hello.o.asm与hello.s的区别

会发现hello.o.asm比hello.s代码多很多。首先hello.o.asm拥有了机器代码,同时汇编代码保持不变;其次,hello.o.asm的每个函数都在重定位后拥有其相应的地址。最后,在hello.o.asm中,我们能看见许多函数名称比如printf@plt <__libc_csu_init>等函数代码,这些函数都是最初程序引用的库函数或者是做出的定义。

4.5 本章小结

本章把c程序的hello.s汇编成hello.o,形成了一个可执行文件hello.o。我们可以生成hello.elf来查看程序都有哪些节,又有哪些重定位条目。也可以通过hello.o.asm来查看最终的机器代码、以及对应的汇编代码。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 /usr/lib/gcc/x86_64-linux-gnu/7/crtbegin.o /usr/lib/gcc/x86_64-linux-gnu/7/crtend.o
/usr/lib/x86_64-linux-gnu/crtn.o
hello.o -lc -z relro -o hello

使用ld的链接命令。

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

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

输入:readelf -a hello > Hello.elf 得到Hello.elf

从程序头中,我们可以看到这个程序一共有8个段。

以PHDR段为例,起始位置的偏移为0x40,虚拟地址为0x400040,物理地址是0x400040,大小为0x1c0,只读,8字节对齐

5.4 hello的虚拟地址空间

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

在edb中打开Symbols窗口,双击相对应的段,来查看相应的机器代码以及地址。

  1. INTERP段:

正如5.3节的节头信息表明,.interp段的地址为0x400200

  1. .rodata段

正如5.3节的节头信息表明,.rodata段的地址为0x4006f0

  1. .data段

正如5.3节的节头信息表明,.data段的地址为0x601040

  1. .bss段

正如5.3节的节头信息表明,.bss段的地址为0x601054

由对比得出,edb得出的信息与elf文件输出的信息相同,由于elf文件中的节头一共有27个段,这里就挑出几个考试相关的段作为参考。

5.5 链接的重定位过程分析

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

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

不同之处:我们可以发现在hello.o文件中每个段以及里面语句的地址的地址都为相对偏移地址,地址都是以一串0开头的。而在已重定位objdump -d -r hello的输出中,每个段以及相应的语句都已经分配了绝对的语句,他们在栈中已经有了相应的位置。.init段首地址也是从0x400488开始。也就是说,hello.o是可重定位目标文件,hello是可执行目标文件。

链接的过程:hello程序调用了printf函数,它是每个C编译器都提供的标准C库中的一个函数,printf函数存在于一个名为printf.o的单独与变异好了的目标文件中,额而这个文件必须以某种方式合并到我们的hello.o程序中。链接器(ld)就负责处理这种合并。结果就得到hello文件,它是一个可执行目标文件(可执行文件),可以被加载到内存中,由系统执行。

重定位的过程:链接器把所有属于同一个节的符号都会聚到一起,形成最后的.data节等。然后连接起将运行是内存地址赋给新的聚合节,赋给输入模块定义的每个节以及赋给输入模块定义的每个符号,当这一步完成时,程序的每条指令和全局变量都有唯一的运行时内存地址了。具体的计算步骤分为重定位相对引用、重定位绝对引用等。例如:.init段的首地址为0x400488,下一句话相对偏移量为4,所以地址为0x40048c。对于地址0x400496处je指令后面的机器指令为下一句地址0x40049a - 0x400498 = 0x2;

5.6 hello的执行流程

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

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

程序名称

程序地址

ld-2.27.so!_dl_start

0x7f92 ce7b2090

ld-2.27.so!_dl_init

0x7f92 ce7c1630

hello!_start

0x400500

-libc-2.27.so!__libc_csu_init

0x400670

hello!_init

0x400488

libc-2.27.so!__sigsetjmp

0x7f92 ce7ce090

libc-2.27.so!__sigjmp_save

0x7f92 ce7cc660

hello!main

0x4005e7

hello!puts@plt

0x4004b0

hello!exit@plt

0x4004e0

ld-2.27.so!_dl_runtime_resolve_xsave

0x7f92 ce7c8680

-ld-2.27.so!_dl_fixup

0x7f92 ce7c0df0

–ld-2.27.so!_dl_lookup_symbol_x

0x7f92 ce7bc0b0

libc-2.27.so!exit

0x7f92 ce7ce030

5.7 Hello的动态链接分析

分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。

编译器没有函数运行时的地址,需要链接器进行链接处理。动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。

在dl_init调用前,调用的目标地址都是实际指向外部库的代码逻辑,GOT存放的是PLT中函数调用指令的下一条指令地址。

在dl_init调用之后,0x601008和0x601010处的两个8B数据分别发生改变,其中变化便是GOT[1]指向重定位表,用来确定调用的函数地址,然后在调用函数时,先跳转到PLT执行.plt中逻辑,然后访问动态链接器确定函数地址,重写GOT(以便再次访问该函数时可直接跳转),将控制传递给目标函数。

5.8 本章小结

本章介绍了链接和重定位的相关知识,连接是将两个可重定位目标文件合并成一个可执行文件。而重定位是将合成的代码通过相对偏移量,得到正确的地址,为了加载到内存来运行文件。

第6章 hello进程管理

6.1 进程的概念与作用

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

它的作用就是向每个程序提供一个假象,好像它在独占的使用处理器。这使CPU同时能运行更多的程序。

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

Linux系统中,Shell是一个交互型应用级程序,代表用户运行其他程序(是命令行解释器,以用户态方式运行的终端进程。

其基本功能是解释并运行用户的指令,重复如下处理过程:

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

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

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

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

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

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

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

6.3 Hello的fork进程创建过程

在终端输入./hello 1180300601 mcj,Shell会对这句话进行解析,因为hello不是Shell内置的命令,于是判断为执行hello程序。Shell的父进程通过fork函数创建一个新的子进程。新创建的子进程几乎但不完全与父进程相同,子进程得到与父进程用户级虚拟地址空间相同的、独立的一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的热门和文件。父进程和新创建的子进程之间最大的区别在于它们有着不同的PID。

6.4 Hello的execve过程

fork函数创建了一个子进程之后,子进程就调用execve函数在当前进程的上下文中加载并运行一个hello这个新程序。首先execve函数的filename为hello,其他参数有参数列表argv和环境变量envp。当加载器运行时,execve创建一个内存映像。相对应的段数据储存在相应的栈地址,比如只读代码段储存.init,.text,.rodata段的数据;运行时堆储存用malloc创建的数据;用户栈在程序运行时向下生长。当hello开始执行时,用户栈从顶向下依次为系统启动函数libc_start_main,argv[0]argv[argc],envp[0]envp[n],之后是以NULL结尾的argv[]数组,其中每个元素都指向栈中的一个参数字符串。

6.5 Hello的进程执行

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

  1. 定义:

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

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

用户态和核心态转换:处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。

  1. 分析

在要hello运行的过程中,若hello进程不被抢占,则直接运行hello程序;如果被抢占,那么就进入内核模式,当上一个进程结束后,hello会成为新的进程。

在hello进程执行的时候,一开始是以用户模式运行的,在调用sleep函数就进入了内核模式,内核将hello的进程释放,并将其加入等待队列,同时定时器开始计时,当定时器计时到相应的秒数2.5s时,就发送一个中断信号,内核进行中断处理,重新将hello程序加入到运行队列,等待成为运行的进程。

6.6 hello的异常与信号理

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

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

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

  1. 不停乱按,包括回车

进程收到的信号:无

收到的异常:

时钟中断:不处理

系统调用:调用write函数打印字符

I/O中断:将键盘输入的字符读入缓冲区

缺页故障(可能):缺页处理程序

  1. Ctrl-Z

执行Ctrl-z操作:

进程收到的信号:SIGSTOP,挂起(停止)

收到的异常:时钟中断、不处理

执行fg操作:

进程收到的信号:SIGCONT,继续运行

收到的异常:时钟中断、不处理

执行kill -9 %1操作:

进程收到的信号:SIGKILL,杀死进程

收到的异常:时钟中断、不处理

  1. Ctrl-C

进程收到的信号:SIGINT,shell使进程终止

收到的异常:时钟中断、不处理

6.7本章小结

本章对进程加深了讨论,从进程的fork创建、execve执行,再到控制流对进程的执行过程都有了一个完整的介绍。解释了hello这个进程是如何通过Shell在系统中执行命令的。

第7章 hello的存储管理

7.1 hello的存储器地址空间

结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。

逻辑地址:由程序产生的与段有关的偏移地址。分为两个部分,一个部分为段基址,另一个部分为段偏移量。

在hello.asm中,寻找全局变量sleepsecs。

其中代码显示的0x601050就是偏移地址。由于全局变量在.text段,此时,DS寄存器存储着段选择符。使用GDB调试,可以查看当前DS寄存器的值:

可知DS中的值为0,即段选择符为0

所以在hello中,对全局变量sleepsec的访问,逻辑地址为

0000 :
601050

线性地址:是逻辑地址到物理地址变换的中间层,是处理器可寻址空间的地址。程序代码产生的偏移地址加上段基地址就产生了线性地址。而在实模式、保护模式下。段基地址有着不同的确定方式。

虚拟地址:CPU 启动保护模式后,程序运行在虚拟地址空间中。与物理地址相似,虚拟内存被组织为一个存放在磁盘上的 N 个连续的字节大小的单元组成的数组,其每个字节对应的地址成为虚拟地址。虚拟地址包括 VPO(虚拟页面偏移量)、VPN(虚拟页号)、TLBI(TLB 索引)、TLBT(TLB 标记)。以 hello 为例,hello 反汇编得到的文件中第 145 行:0000000000400606 :这里的 0x400606 是逻辑地址的偏移量部分,偏移量再加上代码段的段地址就得到了 main 的虚拟地址(线性地址),虚拟地址是现代系统的一个抽象概念,再经过MMU 的处理后将得到实际存储在计算机存储设备上的地址。

物理地址:计算机系统的主存被组织成一个由 M 个连续的字节大小的单元组成的数组,其每一个字节都被给予一个唯一的地址,这个地址称为物理地址。物理地址也是计算机的硬件中的电路进行操作的地址。

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

逻辑地址空间表示:段地址:偏移地址。

在实模式下:逻辑地址 CS:EA=CS*16+EA 物理地址

在保护模式下:以段描述符作为下标,到 GDT/LDT 表查表获得段地址,段地址+偏移地址=线性地址。段内偏移量是在链接后就已经得到的 32 位地址,因此要想由逻辑地址得到线性地址,需要根据逻辑地址的前 16 位获得段地址,这 16 位存放在段寄存器中。其中,段寄存器(16 位):用于存放段选择符;CS(代码段)为程序代码所在段;SS(栈段)为栈区所在段;DS(数据段)为全局静态数据区所在段

其他三个段寄存器 ES、GS 和 FS 可指向任意数据段。

段选择符中字段的含义:
其中 CS 寄存器中的 RPL 字段表示 CPU 的当前特权级:

当TI=0时,选择全局描述符表(GDT),TI=1,选择局部描述符表(LDT)

当RPL=00时为第 0 级,位于最高级的内核态;RPL=11 为第 3 级,位于最低级的用户

高 13 位-8K 个索引用来确定当前使用的段描述符在描述符表中的位置。

段描述符是一种数据结构,等价于段表项,分为两类。一类是用户的代码段

和数据段描述符,一类是系统控制段描述符。

描述符表:实际上为段表,由段描述符(段表项构成)分为三种类型:

全局描述符表 GDT:只有一个,用来存放系统内每个任务都可能访问的描述符,例如,内核代码段、内核数据段、用户代码段、用户数据段以及 TSS(任务状态段)等都属于
GDT 中描述的段。

局部描述符表 LDT:存放某任务(即用户进程)专用的描述符

中断描述符表 IDT:包含 256 个中断门、陷阱门和任务门描述符。

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

概念上而言,虚拟内存被组织为一个由存放在磁盘上的
N 个连续的字节大小的单元组成的数组。每字节都有一个唯一的虚拟地址,作为到数组的索引。磁盘上数组的内容被缓存在主存中。和存储器层次结构中其他缓存一样,磁盘(较低层)上的数据被分割成块,这些块作为磁盘和主存(较高层)之间的传输单元。VM 系统通过将虚拟内存分割位称为虚拟页的大小固定的块来处理这个问题。每个虚拟页的大小位 P = 2p 字节。类似地,物理内存被分割为物理页,大小也为 P 字节。

虚拟地址空间中的每个页在页表中一个固定偏移量的位置都有一个 PTE(页表条目),而每个 PTE 是由一个有效位和一个 n 位的地址字段组成的。页表 PTE 分为三种情况:

已分配:PTE 有效位为 1 且地址部分不为 null,即页面已被分配,将一个

虚拟地址映射到了一个对应的物理地址

未缓冲:PTE 有效位为 0 且地址部分不为 null,即页面已经对应了一个虚

拟地址,但虚拟内存内容还未缓存到物理内存中

未分配:PTE 有效位为 0 且地址部分为 null,即页面还未分配,没有建立

映射关系

虚拟页面地集合被分为三个不相交的子集:已缓存、未缓存和未分配。

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

VA中分为VPN与VPO,其中VPN又分为TLBT(TLB标记)、TLBI(TLB索引)

首先,系统会根据VA中的TLBT与TLBI来访问TLB中的某一组,遍历该组中的所有行,若找到一行的标记位等于TLBT,且有效位为1,则缓存命中,该行存储的即为PPN;如果没有找到一行的标记位符合TLBT,或有效位为0,则缓存不命中。系统从页表中找到被请求的块,用以替换原TLB表项中的数据,并把它作为PPN。

对四级页表的访问:

缓存不命中后,VPN被解释成从低位到高位的4段,从高地址开始,

第一段VPN作为第一级页表的索引,用以确定第二级页表的基址;

第二段VPN作为第二级页表的索引,用以确定第三级页表的基址;

第三段VPN作为第三级页表的索引,用以确定第四级页表的基址;

第四段VPN作为第四级页表的索引,若该位置的有效位为1,则第四级页表中的该表项存储的是所需要的PPN的值。在上述过程中,只要有一级页表条目的有效位为0,下一级页表就不存在,也就是产生缺页故障。

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

物理地址分为CT(缓存标记),CI(组索引),CO(块偏移)三部分。

系统会根据CI、CO在缓存中找到对应的组,遍历该组中的所有行,若找到一行的标记位等于CT,且标志位有效位为1,则缓存命中(hit),根据CO(块偏移)读取块中对应的字;若未找到标记位为CT的行,或该行的有效值为0,则缓存不命中。若缓存不命中,则向下一级缓存中查找数据找到数据之后,开始进行行替换。若该组中有一个空行,那就将数据缓存至这个空行,并置好标记位和有效位;若该组中没有空行,替换过去某个时间窗口内引用次数最少的那一行(LFU)或替换最后一次访问时间最久远的那一行(LRU)。

7.6 hello进程fork时的内存映射

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

7.7 hello进程execve时的内存映射

首先execve函数的filename为hello,其他参数有参数列表argv和环境变量envp。当加载器运行时,execve创建一个内存映像。相对应的段数据储存在相应的栈地址,比如只读代码段储存.init,.text,.rodata段的数据;运行时堆储存用malloc创建的数据;用户栈在程序运行时向下生长。当hello开始执行时,用户栈从顶向下依次为系统启动函数libc_start_main,argv[0]argv[argc],envp[0]envp[n],之后是以NULL结尾的argv[]数组,其中每个元素都指向栈中的一个参数字符串。加载并运行hello 需要以下几个步骤:

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

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

3)映射共享区域,hello 程序与共享对象libc.so 链接,libc.so 是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。

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

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

在MMU 中查找页表时发现与该地址相对应的物理地址不在内存中,这时就发生了故障,因此必须从磁盘中取出相应的数据。如果取出了相应的数据,就重新执行指令,否则终止这项进程。

7.9动态存储分配管理

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

Printf会调用malloc,请简述动态内存管理的基本方法与策略。

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

分配器有两种分别是隐式分配器和显式分配器。隐式分配器是要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。显式分配器是要求应用显式地释放任何已分配的块。

隐式空闲链表:在内存块有头部和脚部,他们都是4字节,储存了块大小以及是否已分配。其中头部用于寻找下一个块,脚部用于寻找上一个块。脚部的目的是合并空闲块。

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

7.10本章小结

本章介绍了hello何从CPU中的虚拟内存中开始,翻译成为物理内存,最后到相应磁盘进行读取数据的过程。此外还对CPU的虚拟地址向物理地址转换的规则进行了解释,以及对缺页和中断的异常进行处理,还有动态储存分配的两种方法。现在,hello已经从虚拟地址转换到物理地址,并最终从磁盘读取到了相应的数据了。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:所有的IO设备都被模型化为文件

设备管理:所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备映射为文件的方式,允许Linux内核引出一个简单,低级的应用接口,称为Unix I/O

8.2 简述Unix
IO接口及其函数

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

Unix IO接口有5种模型分别是阻塞式IO、非阻塞式IO、IO复用、信号驱动式IO、异步IO。

  1. 阻塞式IO:

阻塞式IO模型是最一般的IO模型。在这种模型下,IO函数调用(read & write等等)都会在操作完成或者发生中断以后才会返回。如果指定的操作数据没有就绪,或者操作需要的外部条件(比如缓冲区)尚未符合要求,操作会一直阻塞。

  1. 非阻塞式IO:

相对于阻塞式IO模型,非阻塞式IO的特点就是:当所请求的IO操作暂无法如期完成时,不要把本进程投入睡眠时,而是直接返回一个错误(EWOULDBLOCK)。这样做避免了程序长时间被挂起。但是由于在这种模式下,程序需要不断去轮询,会耗费大量的CPU时间。

  1. IO复用:

IO复用是指在一个IO请求内,等待多个可能的IO对象可用(在unix下由文件描述符来标记)。虽然IO复用还是会导致阻塞,但是当IO操作对象较多时,就能够避免产生很大一部分的阻塞时间。在Unix下,我们可以用select或poll函数实现IO复用。

  1. 信号驱动式IO:

在信号驱动式IO模型下,我们可以让内核在描述符就绪时发送SIGIO信号通知我们。使用这种模型,我们需要首先开启套接字的信号驱动式IO功能,并通过sigaction系统调用来安装一个信号处理函数。

  1. 异步IO:

异步IO的特点是:让内核启动某个IO操作,并让内核在这个操作完成后通知我们。在这期间,调用异步IO操作的进程不会被阻塞。异步IO与信号驱动式IO的区别在于:信号驱动式IO是由内核通知我们何时可以启动IO操作,而异步IO模型是由内核通知我们操作何时完成。

Unix IO的函数有:

int
open(char* filename,int flags,mode_t mode) :进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位

int
close(fd),fd是需要关闭文件的描述符

ssize_t
read(int fd,void *buf,size_t n),该函数从描述符为fd的当前位置最多赋值n个字节到内存buf的位置,返回值为实际传送的字节数量

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

8.3 printf的实现分析

https://www.cnblogs.com/pianist/p/3315801.html

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

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

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

研究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;

}

va_list的定义:typedef char
va_list,说明它是一个字符指针,其中 (char)(&fmt) + 4) 即arg表示的是…中的第一个参数。

研究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); 

}

i =
vsprintf(buf, fmt, arg); 。vsprintf返回的是一个长度,返回的是要打印出来的字符串的长度。

8.4 getchar的实现分析

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

getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。同时同时产生中断请求,请求抢占当前进程运行键盘中断子程序,中断子程序先从键盘接口取得该按键的扫描码。当接收到回车输入后,程序才继续开始返回进程。

8.5本章小结

本章主要介绍了Unix IO的接口与相关函数。以及printf函数、getchar函数的原理。

结论

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

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

Hello的一生所经历的过程:

  1. 预处理:将hello.c所有的头文件的相关代码合并成一个hello.i文件。

  2. 编译:将hello.i或hello.c文件编译成汇编文件hello.s, c语言变成汇编语言

  3. 汇编:将hello.s翻译成可重定位目标文件hello.o, 汇编语言加上机器代码

  4. 链接:将hello.o与库函数连接成可执行目标程序hello

  5. Shell运行:在Shell中输入运行指令./hello 1180300601 马铖君。首先Shell判断它不是内部指令,于是fork了一个子进程调用execve函数运行hello程序

  6. 进程:Hello成为进程之后,也许会接受到很多信号,比如说ctrl+z和ctrl+c分别表示挂起(SIGSTP)和终止(SIGINT)的信号。内核会根据不同的信号,对hello这个进程进行相应的逻辑控制流。

  7. 访存:内存管理单元(MMU)将逻辑地址转变为虚拟内存地址、再由虚拟内存地址转换为物理内存地址,从物理内存地址一步步向上一级储存结构进行寻找相关数据。直到找到相关的数据。

  8. 申请动态内存:printf语句会调用malloc想动态内存分配器申请堆中的内

存。

  1. 终止与回收:最终所有进程的父进程来回收这个子进程,内核中将删除这个进程创建的所有数据,Hello的一生到此终止。

附件

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

hello.c:hello程序c语言源文件

hello.i:hello.c预处理生成的文本文件

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

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

hello.elf:使用readelf读取hello.o得到的信息

hello.o.asm:使用objdump反汇编hello.o得到的反汇编代码

hello:由hello.o与动态链接库链接得到的可执行目标文件

Hello.elf:使用readelf读取hello得到的信息(这个是大写H的命名)

hello.asm:使用objdump反汇编hello得到的反汇编代码

参考文献

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

[1] 大卫R.奥哈拉伦,兰德尔E.布莱恩特. 深入理解计算机系统[M]. 机械工业出版社.2018.4

[2] printf 函数实现的深入剖析:

https://www.cnblogs.com/pianist/p/3315801.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值