程序人生-Hello’s P2P

计算机系统

大作业

题     目   程序人生-Hello’s P2P   

专       业   计算学部                 

学     号   1190302020                

班     级   1903009                   

学       生   魏志豪                 

指 导 教 师   吴锐                      

计算机科学与技术学院

2021年5月

摘  要

本论文从预处理、编译、汇编、链接、进程、虚拟内存、I/O等方面分析了hello从C语言文件到在系统中执行的全过程。

关键词:预处理;编译;汇编;链接;进程;I/O;虚拟内存                           

目  录

第1章 概述... - 4 -

1.1 Hello简介... - 4 -

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

1.3 中间结果... - 4 -

1.4 本章小结... - 5 -

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

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

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

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

2.4 本章小结... - 8 -

第3章 编译... - 9 -

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

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

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

3.4 本章小结... - 14 -

第4章 汇编... - 15 -

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

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

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

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

4.5 本章小结... - 20 -

第5章 链接... - 21 -

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

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

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

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

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

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

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

5.8 本章小结... - 26 -

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

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

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

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

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

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

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

6.7本章小结... - 29 -

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

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

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

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

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

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

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

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

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

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

7.10本章小结... - 34 -

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

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

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

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

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

8.5本章小结... - 36 -

结论... - 37 -

附件... - 38 -

参考文献... - 39 -

第1章 概述

1.1 Hello简介

1.1.1 From Program to Process(P2P)

Hello程序的生命周期是从一个高级C语言程序开始的,其经过预处理阶段、编译阶段、汇编阶段、链接阶段这四个阶段,实现了从一个hello.c文件到一个可执行二进制文件hello的蜕变。

图1-1 从hello.c到hello的全过程

之后在通过shell中输入命令,通过shell来fork一个子进程,子进程通过调用execve函数来执行hello文件。上述的全过程就是hello的P2P。

1.1.2 From Zero-0 to Zero-0(020)

最开始是没有Hello程序的进程存在的,shell通过fork和execve添加了一个hello进程,并为其分配唯一pid;在hello走完自己短暂的一生后,shell会帮hello收拾尸体(即回收进程并删除有关进程的上下文)。这就是hello的020过程。

1.2 环境与工具

1.2.1 硬件环境

      X64 CPU;2GHz;8G RAM;512GHD Disk

1.2.2 软件环境

      Windows10 64位; Vmware 16;Ubuntu 16.04 LTS 64位

1.2.3 开发与调试工具

      edb,VS 2019

1.3 中间结果

hello:链接后生成的可执行文件

hello.o:汇编后生成的可重定位目标文件

hello.i:预处理后的文件

hello.s:编译后的文件

helloelf.txt:使用objdump生成的hello可执行文件的反汇编文件

relhello.txt:使用objdump生成的hello.o的反汇编文件

1.4 本章小结

       本章介绍了hello的P2P、020过程、实验的环境与工具以及实验过程中产生的中间文件。

第2章 预处理

2.1 预处理的概念与作用

2.1.1 预处理的概念

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

2.1.2 预处理的作用

      预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如大作业hello.c文件中的这几个在预处理过后都会被其中的内容所替换掉。

图2-1  hello.c中需要预处理的代码

2.2在Ubuntu下预处理的命令

预处理命令:gcc -E hello.c -o hello.i

图2-2  ubuntu下的预处理命令

2.3 Hello的预处理结果解析

可以看到#开头的几个包含头文件的命令都被替换成了头文件相应的路径和头文件里的内容(包括一些函数和结构体的定义,还有一些typedef重命名)。

图2-3  hello.i文件的内容

我们还可以看到原来hello.c文件里的main函数是没有发生变化的。

图2-4  hello.i文件里的main函数

2.4 本章小结

本章介绍了预处理的概念和作用以及ubuntu系统下的预处理命令,并对hello.c预处理后得到的hello.i文件的内容进行了分析。

第3章 编译

3.1 编译的概念与作用

3.1.1 编译的概念

编译器(ccl)将预处理生成的.i文件中的代码翻译成相应的汇编代码,并生成对应的文本文件hello.s(此时的hello.s本质上还是由ASCII码组成的),它包含一个汇编语言程序。

3.1.2 编译的作用

      编译将C语言翻译成了汇编语言,为之后生成二进制机器码的文件打下了基础。

3.2 在Ubuntu下编译的命令

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

图3-1  ubuntu下的编译命令

3.3 Hello的编译结果解析

3.3.1 常量

      编译器将C语言中的两个字符串常量分别存到了.LC0和.LC1字段里

图3-2  hello.c文件中的字符串常量

图3-3  hello.s文件中的字符串常量

3.3.2 局部变量

编译器将C语言中的局部变量int i存到了main函数的栈帧里,由于i起初没有赋初值,我们只有在对i进行赋值时才能找到编译器是如何处理局部变量i的,在hello.c文件中对i的赋值是在for循环里进行的,于是我们找到for循环在hello.s里对应的位置,就可以找到i在栈帧中具体的储存位置,即-4(%rbp)。

图3-4  hello.c文件里的局部变量

图3-5  局部变量i的第一次赋值

图3-6 局部变量i的具体位置

3.3.3 赋值

编译器会将C语言的赋值操作用汇编语言中的mov命令来表示,例如movl $0,-4(%rbp),就代表着i=0的赋值语句,其中movl中的l代表这是一个4个字节的赋值操作(因为int型是4字节大小的),$0是立即数就代表着0,而-4(%rbp)在之前已经分析过就是局部变量i的存储位置。

       3.3.4 关系

编译器会将cmp命令和有条件跳转命令相结合来实现各种不同的关系操作。在hello.c中一共出现了两个关系运算符:!=和<,它们在hello.s中的表示如下图所示:

图3-7  hello.s中!=的表示

图3-8  hello.s中<的表示

我们就<的表示来具体分析,cmpl $7,-4(%rbp)是用来设置条件码的(条件码是之后jle跳转条件是否成立的关键),jle .L4是有条件的跳转指令,当-4(%rbp)中的值小于等于7时才会进行跳转,跳转到.L4节,否则会按照顺序执行下一条汇编指令即call getchar。

3.3.5 参数传递

编译器会将给被调用函数传递的参数存在对应的寄存器中,若参数过多,多出的参数会存入栈中。在hello.c中有main函数、printf函数、sleep函数以及exit函数的参数传递,在此我们就挑出printf函数对参数传递的处理进行分析。

图3-9  hello.c里printf函数的参数传递

我们从图3-9可以看到main函数一共向printf函数传递了3个参数分别是”Hello %s %s\n”、argv[1]、argv[2],编译器对参数传递的处理如图3-10所示

图3-10  hello.s中printf的参数传递

我们对上图中的汇编语句进行一一分析,movq -32(%rbq),%rax是将argv的首地址即&argv[0]存入寄存器rax中,addq $16,%rax是%rax=%rax+16,即:使rax里保存的指针指向argv[2],movq (%rax),%rdx(这是第一个划线语句)是将argv[2][0]的地址存入%rdx中,即实现了第三个参数argv[2]的传入且argv[2]保存在寄存器%rdx中,同理可以知道movq %rax,%rsi实现了第二个参数argv[1]的传递并将其保存在寄存器%rsi中,而movl $.LC1,%edi就实现了第一个参数”Hello %s %s\n”的传递。

3.3.6 函数调用

编译器会使用call命令来处理函数的调用,即先通过3.3.5节中分析的相关命令进行参数传递后,再使用call命令进行函数的调用。

图3-11  hello.c中printf函数的调用

图3-12  hello.s中printf函数的调用命令call

3.3.7 函数返回

编译器是使用leave和ret命令来实现函数返回的。leave用来释放栈帧和还原%rsp;ret用来返回%rax的值相当于return命令。

图3-13  hello.c中的函数返回

图3-14  hello.s中的函数返回

3.3.8 算术操作++

编译器使用add命令来实现++操作,例如hello.s里的addl $1,-4(%rbp)命令,该命令的原理是将-4(%rbp)里存的内容加1后再存入-4(%rbp)中, 它实现的就是i=i+1(即i++)的C语言代码。

图3-15  hello.c中的++操作

图3-16  hello.s中的++操作

3.3.9 数组和指针操作

编译器通常使用寄存器或者栈帧来保存指针。在hello.s里编译器将指针数组argv的首地址存到了栈帧中,并通过栈帧来实现指针的操作,正如3.3.5参数传递中分析的那样。

图3-17  hello.c中字符串指针数组argv的使用

3.3.10 控制转移

编译器通过关系比较和算术操作相结合来实现for循环,通过关系比较来实现if语句。

图3-18  hello.c中的if语句

图3-19  hello.s中的if语句

通过图3-18,我们可以看到当argv不等于4时执行if语句,对此我们主要对图3-19进行分析,cmpl命令是将argv和4进行运算并设置条件码,之后的je命令根据之前设置的条件码判断是否跳转,若argv等于4就跳转到.L2(即不执行循环),否则就会执行图片上后面的语句(即if里面的语句)。

3.4 本章小结

本章介绍了编译的概念和作用及ubuntu中编译的命令,并对编译器如何将hello.c中的C语言翻译为汇编语言进行了分析。

第4章 汇编

4.1 汇编的概念与作用

       4.1.1汇编的概念

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

      

4.1.2汇编的作用

汇编将原来的由ASCII表示的文件翻译为了二进制机器语言的文件,这使hello程序离诞生又更近了一步。

4.2 在Ubuntu下汇编的命令

汇编的命令:gcc -c -m64 -no-pie -fno-PIC hello.s -o hello.o

图4-1  ubuntu下汇编的命令

4.3 可重定位目标elf格式

hello.o的ELF文件主要由以下几个节(未包括全部节)组成:

1 ELF头:生成该文件的系统的字的大小和字节顺序,帮助链接器语法分析和解释目标文件的信息。

图4-2  hello.o的ELF头

2 .text:已编译程序的机器代码,hello.o的.text节中只有main函数的机器语言指令,下图是使用objdump得到的hello.o里的.text节的内容。

图4-3  hello.o的.text节

3 .rodata:只读数据,hello.o中的字符串常量就储存在这个节里。

图4-4  hello.o的.rodata节

4 .data:已初始化的全局和静态变量,hello里没有全局变量和静态变量,所以这个节为空。

5 .bss:未初始化的全局和静态变量,以及所有被初始化为0的全局或静态变量,hello里没有全局变量和静态变量,所以这个节也为空。

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

图4-6  hello.o的.symtab节

7 .rel.text:一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。

图4-4  hello.o的.rela.text节

8 .rel.data:被模块引用或定义的所有全局变量的重定位信息。hello没有引用和定义全局变量,所以这个节也为空。

9 节头部表:在这个表里可以看到hello.o里所有节大小和位置的信息。

图4-3  hello.o的节头部表

4.4 Hello.o的结果解析

通过hello.s和objdump得到的重定向反汇编文件relhello.txt的对比,我们可以看到原汇编程序和机器语言反汇编得到的汇编代码有以下区别:

1 在汇编程序中操作数是十进制的,反汇编后操作数变成了16进制数,但值还是一样的。

2 在汇编程序中代码是分成若干节组成的(是由.LFB2、.L2、.L3、.L4组成的main函数),反汇编后这几块都合成了一个整体。

3 在汇编程序中调用函数是直接使用call+函数名字来表示的,反汇编后call后面变成了重定位信息,并使用机器码的00 00 00 00来表示这在之后的链接中需要重定位。

4 在汇编程序中分支转移是跳转命令+对应的节(如:.L2、.L3等)来实现的,反汇编后直接变成了跳转命令+跳转位置在main函数中的相对位置(例如je 29,就是在条件满足时跳转到main函数中的29行处)

图4-4  objdump得到的反汇编文件relhello.txt里的main函数

图4-5  hello.s里的main函数

4.5 本章小结

本章介绍了汇编的概念和作用及ubuntu中汇编的命令,并对hello.s中的汇编语言和objdump反汇编hello.o得到的汇编语言的区别进行了分析。

5章 链接

5.1 链接的概念与作用

5.1.1 链接的概念

      链接是链接器将编译阶段编译器生成的可重定位二进制文件转化为可执行二进制目标文件的过程。

5.1.2 链接的作用

      链接会将目标文件使用到的静态库、动态库或者是其他可重定位二进制文件链接到一起,在完成链接这步后hello程序就正式诞生了(即实现了从ASCII码的hello.c文件到二进制机器码的可执行文件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

图5-1  ubuntu下的链接命令

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

图5-2  hello文件的ELF头

       通过节头部表可以得知各段的基本信息,包括各段的起始地址,大小等信息。

图5-3  hello文件的节头部表

5.4 hello的虚拟地址空间

       通过节头部表中的节起始位置和大小可以知道相应的节在虚拟内存中的位置。例如:.init节在0x401000处,大小为0x1b,所以.init在虚拟内存中的位置如图5-4所示

图5-4 .init节在虚拟内存中的位置

图5-5  edb加载hello

5.5 链接的重定位过程分析

5.5.1 hello与hello.o的不同

使用objdump得到hello的反汇编文件helloelf.txt。在helloelf.txt中函数的调用使用plt和got的配合来实现,并且call使用的是相对寻址,跳转的位置是其对应的plt的位置;需要重定位的字符串常量也换成了重定位后字符串常量的地址;relhello.txt汇编命令前的相对位置,也变成了虚拟内存中的绝对位置。如图5-6、5-7、5-8、5-9所示。

图5-6  relhello.txt中的字符串常量

图 5-7  helloelf.txt中的字符串常量

图 5-8  relhello.txt中的函数调用

图5-9  helloelf.txt中的函数调用

5.5.2 hello的重定位

通过之前得到的重定位项目,我们可以知道,hello.o里的函数都是相对寻址的的(R_X86_64_PC32),所有的字符串常量都是绝对寻址的(R_X86_64_32),在链接器链接时它会找到符号表里的对应函数或字符串常量的位置,并根据重定位的类型将重定位后的信息存入相应的位置中。相对寻址会将当前PC的值与目标函数地址的值作差后存入预留的位置中;绝对寻址是找到目标字符串的地址值,然后直接将地址存进去。

图5-10  hello.o的重定位代码节

5.6 hello的执行流程

1  0x00000000004010f0: hello!_start

2  0x00007fbd284acfc0: libc-2.31.so!__libc_start_main

3  0x00000000004011c0: hello!__libc_csu_init

4  0x00007fbd2868d010: ld-2.31.so!_dl_catch_exception@plt

5  0x0000000000401000: hello!.init

6  0x00007fbd284cbe00: libc-2.31.so!_setjmp

7  0x00007fbd285d1210: libc-2.31.so!_seterr_reply

8  0x0000000000401125: hello!main

9  0x0000000000401030: hello!puts@plt

10  0x0000000000401040: hello!printf@plt

11  0x0000000000401050: hello!getchar@plt

12  0x0000000000401060: hello!atoi@plt

13  0x0000000000401070: hello!exit@plt

14  0x0000000000401080: hello!sleep@plt

15  0x0000000000401230: hello!__libc_csu_fini

16  0x0000000000401238: hello!.fini

5.7 Hello的动态链接分析

在dl_init调用之前,对于每一条PIC函数调用,调用的目标地址都实际指向PLT中的代码逻辑,初始时每个got条目都指向对应的PLT条目的第二条指令,如图5-6、5-7所示。

图5-11  got在虚拟内存中的位置

图5-12  调用前got的内容

调用之后,got条目里会存入指向要调用的目标程序的指针,如图5-8所示(注意5-7中的划线部分在5-8中的表示)。

图5-13  调用后got的内容

5.8 本章小结

本章介绍了链接的概念和作用及ubuntu中链接的命令,并对链接后的可执行文件hello的ELF格式和链接的重定位过程进行了分析,之后又讨论了动态链接的实现方法。

6章 hello进程管理

6.1 进程的概念与作用

6.1.1 进程的概念

进程是一个执行中程序的实例,是具有独立功能的程序关于某个数据集合上的一次运行活动,是系统进行资源分配和调度的独立单位。

6.1.2 进程的作用

通过进程,我们会得到一种假象,好像我们的程序是当前唯一运行的程序,我们的程序独占处理器和内存,我们程序的代码和数据好像是系统内存中唯一的对象。

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

6.2.1 Shell-bash的作用

shell是系统的用户界面,提供了用户与内核进行交互操作的一种接口。它接收用户输入的命令并把它送入内核去执行。

6.2.2 Shell-bash的处理流程

shell首先检查命令是否是内部命令,若不是再检查是否是一个应用程序。然后shell在搜索路径里寻找这些应用程序(搜索路径就是一个能找到可执行程序的目录列表)。如果键入的命令不是一个内部命令并且在路径里没有找到这个可执行文件,将会显示一条错误信息。如果能够成功找到命令,该内部命令或应用程序将被分解为系统调用并传给Linux内核。

6.3 Hello的fork进程创建过程

在shell中输入./hello命令后,shell会判断这是一个执行hello可执行文件的命令,之后就会fork一个子进程供hello使用。

6.4 Hello的execve过程

前面fork的子进程通过execve系统调用启动加载器,加载器删除子进程现有的虚拟内存段,并创建hello的代码、数据、堆和栈段。新的栈和堆段被初始化为0.通过将虚拟地址空间的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件的内容。最后,加载器跳转到_start地址,它最终会调用hello的main函数。

6.5 Hello的进程执行

处理器并不会在一段时间里都执行一个程序,而是会采用并发的执行方式,在执行Hello进程时,有可能会发生控制流的转换即进程上下文的切换,会先从执行hello的用户模式进入内核模式,将hello进程的上下文保存好后,再从内核模式切换到执行其他进程的用户模式,从而就实现了上下文的切换。

6.6 hello的异常与信号处理

一定会出现的异常:陷阱和故障

可能会出现的异常:中断和终止

可能产生的信号:SIGINT、SIGTSTP

6.6.1 乱按(包括回车)

             这种情况并不会产生信号。

图6-1 不停乱按(包括回车)

6.6.2 Ctrl-C

      产生SIGINT信号,默认的处理是终止。

图6-2  Ctrl-C

6.6.3 Ctrl-Z

      产生SIGTSTP信号,默认的处理是停止直到下一个SIGCONT信号的到来。

图6-3  Ctrl-Z时的各种命令

6.7本章小结

本章介绍了进程的概念、作用和壳Shell-bash的作用与处理流程以及hello的fork,execve的过程,最后分析了hello进程的异常及信号处理。

7章 hello的存储管理

7.1 hello的存储器地址空间

7.1.1 逻辑地址:

  在有地址变换功能的计算机中,访问指令给出的地址 (操作数) 叫逻辑地址,也叫相对地址。要经过寻址方式的计算或变换才得到内存储器中的物理地址。

7.1.2 物理地址:

  在存储器里以字节为单位存储信息,为正确地存放或取得信息,每一个字节单元给以一个唯一的存储器地址,称为物理地址(Physical Address),又叫实际地址或绝对地址。

7.1.3 虚拟地址:

  CPU启动保护模式后,程序运行在虚拟地址空间中。注意,并不是所有的“程序”都是运行在虚拟地址中。CPU在启动的时候是运行在实模式的,Bootloader以及内核在初始化页表之前并不使用虚拟地址,而是直接使用物理地址的。

7.1.4 线性地址:

  线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。

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

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

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

 这里面,我们只用关心Base字段,它描述了一个段的开始位置的线性地址,offset是偏移量,把Base + offset,就是要转换的线性地址了。

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

线性地址即虚拟地址,用VA来表示。VA被分为虚拟页号(VPN)与虚拟页偏移量(VPO),CPU取出虚拟页号,通过页表基址寄存器(PTBR)来定位页表条目,先通过VPN在TLB找对应的条目若TLB命中则直接取出对应的PPN,若不命中,再到内存中的PTE中寻找对应的页表条目,在有效位为1时,从页表条目中取出信息物理页号(PPN),通过将物理页号与虚拟页偏移量(VPO)结合,得到由物理地址(PPN)和物理页偏移量(PPO)组合的物理地址。

图7-1  Core i7的地址翻译

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

如7.3中所说先到TLB找是否有与VA匹配的PTE若有则直接取出PPN,若没有在到页表中找,由于是四级页表,我们把VPN分成4部分,分别是VPN1、VPN2、VPN3、VPN4,这几部分依次对应着一级页表、二级页表、三级页表、四级页表的索引,之后通过这几个VPN段,依次从一级页表、二级页表、三级页表、四级页表找到对应的PTE,最终在四级页表的PTE里得到对应的PPN。如图7-1。

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

通过7.4的操作得到物理地址后,使用物理地址到高速缓存中L1、L2、L3找对应的缓存,若在L1中命中则直接取出需要的字节,若不命中则到L2中寻找,若在L2中命中则根据LRU策略选取L1中不常用的一行替换掉,之后再返回需要的字节,在L3中同理。

7.6 hello进程fork时的内存映射

当fork 函数被shell调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID。为了给hello进程创建虚拟内存,它创建了hello进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制(此时对象是私有对象)。当fork 在hello进程中返回时,hello进程现在的虚拟内存刚好和调用fork 时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,这个写操作就会触发一个保护故障,当故障处理程序注意到保护异常是由于进程试图写私有的写时复制区域的一个页面引起的,它就会在物理内存中创建这个页面的一个新副本,更新页表条目指向这个新的副本,因此,fork后的hello享有着私有的地址空间。

图7-2  私有对象的写时复制

7.7 hello进程execve时的内存映射

前面fork的子进程通过execve系统调用启动加载器,加载器删除子进程现有的虚拟内存段,并创建hello的代码、数据、堆和栈段。新的栈和堆段被初始化为0.通过将虚拟地址空间的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件的内容。最后,加载器跳转到_start地址,它最终会调用hello的main函数。

图7-3  加载器映射地址空间

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

正如7.4所讨论的,若PTE的有效位为0就会引发缺页故障,这时由缺页故障处理程序将磁盘中的页取出存到DRAM中,并更新页表条目,当页加载到主存中后会返回原程序重新执行引发缺页故障的那条指令,这时指令就可以正常执行了。

7.9动态存储分配管理

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

分配器有两种基本风格:显式分配器和隐式分配器。两种风格都要求应用显式地分配块。它们的不同之处在于由哪个实体来负责释放已分配的块。

1.显式分配器:要求应用显式地释放任何已分配的块。例如,C程序提供一种叫做malloc程序包的显式分配器。C程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块。C++的new和delete操作符与C中的malloc和free相当。

2.隐式分配器:另一方面,要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器,而自动释放未使用的已分配的块的过程叫做垃圾收集。例如,诸如Lisp、ML以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。

7.10本章小结

本章介绍了Intel逻辑地址到线性地址的变换-段式管理,并从动态存储分配管理、缺页故障与缺页中断处理、hello进程execve时的内存映射、hello进程fork时的内存映射、三级Cache支持下的物理内存访问等方面分析了hello的存储管理机制。

8章 hello的IO管理

8.1 Linux的IO设备管理方法

所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备映射为文件的方式,允许Linux内核引出一个简单低级的应用接口,称为Unix I/O。

设备的模型化:文件

设备管理:unix io接口

8.2 简述Unix IO接口及其函数

打开文件——open():open函数将file那么转换为一个文件描述符并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。

关闭文件——close():当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。

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

写入文件——write():write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。

8.3 printf的实现分析

printf接受一个fmt的格式,然后将匹配到的参数按照fmt格式输出。在printf调用了两个函数,一个是vsprintf,还有一个是write。其中vsprintf函数作用是接受确定输出格式的格式字符串fmt(输入),用格式字符串对个数变化的参数进行格式化,从而产生格式化输出;而write函数的作用是将buf中的i个元素写到终端。

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

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

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

图8-1  printf函数的函数体

8.4 getchar的实现分析

当程序调用getchar时,程序就等着用户按键,用户输入的字符被存放在键盘缓冲区中,直到用户按回车为止(回车字符也放在缓冲区中)。当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符,getchar函数的返回值是用户输入的字符的ASCII码,如出错返回-1,且将用户输入的字符回显到屏幕,如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取,即后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为后,才等待用户按键。

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

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

8.5本章小结

本章介绍了Linux的IO设备管理方法和Unix IO接口以及Unix IO函数,并对printf和getchar函数的具体实现进行了分析。

结论

hello所经历的过程:

1 hello起初是用c语言编写好的.c文件。

2 经过预处理器处理后由hello.c变成了hello.i。

3 经过编译器处理后由原来的C语言书写的文件转变为了汇编语言书写的文件。

4 经过汇编器的处理hello由原来的ASCII码文件变为了可重定位的二进制文件hello.o。

5 经过链接器的处理产生了可执行的二进制文件hello,此时的hello就可以登上舞台表演了。

6 壳shell给hello程序fork一个子进程

7 使用execve将hello映射到子进程中

8 执行hello

9 运行结束并被回收

心得体会:

       这看似简单的hello程序从诞生到消亡其实经历了很多复杂的步骤,只有在操作系统、硬件、软件的巧妙配合下,hello程序才能真正地绽放光彩,由此可以看出计算机系统的强大,这是凝结了前人们智慧的结晶的产物,要想真正细致地了解计算机系统,我们要走的路还有很远。

附件

hello:链接后生成的可执行文件

hello.o:汇编后生成的可重定位目标文件

hello.i:预处理后的文件

hello.s:编译后的文件

helloelf.txt:使用objdump生成的hello可执行文件的反汇编文件

relhello.txt:使用objdump生成的hello.o的反汇编文件

参考文献

[1] 预处理_百度百科 (baidu.com)

[2] 链接的一般概念_悟的技术博客_51CTO博客

[3] Randal E.Bryant,David R.O’Hallaron 深入理解计算机系统

[4] [转]printf 函数实现的深入剖析 - Pianistx - 博客园 (cnblogs.com)

[5] getchar()函数具体是什么函数?_百度知道 (baidu.com)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值