Hello的一生

摘  要

本文从计算机底层实现的角度,阐述了hello程序从编写程序、预处理、编译、汇编、链接一直到运行,从外部存储设备、经过I/O桥、进入内存及各级cache、在I/O输出、最终被回收的过程。这是一个最简单的程序,却和最复杂的程序有着同样的生命周期经历。本文从一个程序员的视角展示了计算机系统的机制。

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

 

第1章 概述

1.1 Hello简介

Hello程序的生命周期始于一个源程序,即程序员通过编辑器创建并保存的文本文件。我们将其命名为hello.c。

P2P:From Program to Process。hello.c经过cpp的预处理变成.i文件、ccl的编译变成.s文件、as的汇编变成可重定位目标文件.o、ld的链接成为可执行目标文件hello。在shell中键入启动命令后,shell通过fork产生子进程并且在子进程中execve程序hello,使其得到mmap映射和时间片,hello成为了一个流水线上执行的进程。

020:From Zero-0 to Zero-0。从原来OS存储管理,向cache发出请求,发生缺页故障后,逐层申请,发生一系列的不命中后,通过页面交换进入内存,就这样hello离开磁盘,通过I/O桥,开始了它的一生。再经过上面P2P的一系列过程之后,shell父进程负责回收hello进程,内核删除相关数据结构,hello的一生结束。

1.2 环境与工具

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

1.2.1 硬件环境

X64 CPU(Core i7-8550U 1.80GHz),8GB RAM,512GB固态硬盘

1.2.2 软件环境

Windows 10 64位;Vmware下Ubuntu18.04 64位

1.2.3 开发工具

Ubuntu18.04;Code::blocks

1.3 中间结果

hello.i——预处理产生的文本文件

hello.s——编译产生的汇编文件

hello.o——汇编产生的可重定位目标文件

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

helloo.objdmp——Hello.o的反汇编代码

helloo.elf ——Hello.o的ELF格式

hello.objdmp ——Hello的反汇编代码

hello.elf ——Hello的ELF格式

1.4 本章小结

本章主要简单介绍了hello的P2P,020过程,列出了本次实验环境、中间结果。

接下来将详细介绍hello短暂的一生,从hello.c说起。

第2章 预处理

2.1 预处理的概念与作用

概念:预编译又称预处理,做代码文本的替换工作。是根据以字符#开头的命令,修改源程序的过程,最后会生成.i的文本文件。C语言预处理主要包括3个方面:1.宏定义;2.文件包含;3.条件编译。预处理即将宏进行展开。

作用:

1.#define 标识符 字符串

如上格式的宏定义中,预处理的过程中,将标识符用字符串替代。倘若含有参数,形如#define 宏名(参数表)字符串,则还要做参数替换,但不计算不进行语法检查。

2. #include<文件名>

根据#修改文件后,编译时就以包含处理以后的文件为编译单位,被包含的文件将作为源文件的一部分被编译。

3.条件编译

有些语句希望在条件满足时才编译,还有些语句当标识符已经定义时才编译。

删除所有的注释“//”和“/* */”。添加行号和文件名标识,以便编译器产生调试用的行号信息。保留所有的#pragma编译指令(编译器需要用) 

2.2在Ubuntu下预处理的命令

命令行:linux> gcc -E -o hello.i hello.c

或:cpp hello.c>hello.i

结果:

2.3 Hello的预处理结果解析

 

经过预处理之后,hello.c文件生成一个没有宏定义、没有条件编译指令、没有特殊符号的输出文件.i。原文件中的宏进行了宏展开,头文件中的内容被包含进该文件中,输出文件中只有常量(数字、字符等)和变量的定义。打开该文件可以发现,文件长度变为3060行。文件的内容增加,且仍为可以阅读的C语言程序文本文件。

2.4 本章小结

预处理过程做了代码文本的替换工作,是为编译做的预备工作的阶段,对源程序并没有进行语法检查等操作。通过宏展开宏替换,插入头文件等操作,使得程序中的宏引用被递归地替换掉,从而从hello.c到hello.i。生成的程序在这时还是一个可读的C语言风格的文本文件。完成本阶段转换后,可以进行下一阶段的汇编处理。

第3章 编译

3.1 编译的概念与作用

概念:把用高级程序设计语言书写的源程序,翻译成等价的汇编语言格式目标程序的翻译程序。即,将预处理过程后得到的预处理文件(如hello.i)进行词法分析,语法分析,语义分析并且优化,生成汇编代码文件。

作用:编译程序的基本功能是把源程序(高级语言)翻译成目标程序。除了基本功能之外,编译程序还具备语法检查、调试措施、目标程序优化、不同语言合用、修改手段、覆盖处理等重要功能。

3.2 在Ubuntu下编译的命令

命令行:linux> gcc -S hello.i -o hello.s

或直接从.c文件开始:gcc -S hello.c -o hello.s

3.3 Hello的编译结果解析

3.3.1全局变量,局部变量,指针数组(数据的声明和赋值)

局部变量存放在栈中,通过指向栈底的寄存器偏移量间接寻址获得,存放在%rdi与%rsi当中。待打印的string类型串“Usage: Hello 120L021002 马翊轩”存放在.string节中,argc, *argv参数局部变量存放在栈中,用寄存器存放地址。

3.3.2局部变量(整型)

i被用在了for循环中。如果是局部变量,在我们进行编译时,编译器会选择将其放在栈中或者是放在寄存器中。因为它是循环前判断而且每次都加一,它是放在栈中。

在hello.c的main函数中,传入的第一个参数argc,是我们输入命令行的字符串参数的数目的统计。它被直接放在寄存器%edi中,因为一会需要使用rdi进行传递参数给puts函数,它又被放到了栈帧中即-20(%rbp)。

3.3.3运算

加法:addx 操作数1,操作数2

减法:subx 操作数1,操作数2

3.3.4 if条件语句判断

比较:cmpl 操作数1,操作数2

条件跳转语句:jxx

3.3.5数组

在main函数的参数中有argv[]这个数组作为参数,存放我们在命令行中输入的字符串。hello.s的argv的首地址被存放在寄存器%rsi中,后来被存放在栈中空出的寄存器,便于我们的后续调用其他函数使用。通过偏移地址+基址寻址来确定地址。

3.3.6参数传递

通过栈底指针间接寻址,暂时存放在寄存器%rdi,%rsi中。

3.3.7函数调用和返回

call 函数名,在调用前准备好参数,存放在寄存器%rdi, %rsi当中,并将返回值寄存器初始化movl $0,%eax。pushq %rbp,将上一个栈顶地址压栈,为函数返回时出栈做准备。通过%rax 返回返回值。

3.4 本章小结

编译器通过词法分析和语法分析,在确认所有的代码都符合语法规则之后,对我们的代码做一些优化,将程序翻译成汇编代码,生成我们需要的hello.s文件。

对于一些操作(例如for循环和if判断),编译器会智能地简化过程,并将其转换为汇编语言的控制逻辑;编译器还可以证明函数的调用和跳转,并在堆栈框架和操作中平衡它们。

第4章 汇编

4.1 汇编的概念与作用

汇编是把汇编语言程序翻译成与之等价的机器语言程序的过程。生成每一个汇编语句与机器指令基本是一一对应关系。在汇编时,汇编器(as) 将hello.s 翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o 中。汇编相对于编译过程比较简单,根据汇编指令和机器指令的对照表一一翻译即可。

4.2 在Ubuntu下汇编的命令

命令行:linux> gcc -c hello.s -o hello.o

如果是从C程序文件开始则是linux> gcc -c hello.c -o hello.o

4.3 可重定位目标elf格式

指令:readelf -a hello.o > elf.txt

  获得hello的elf文件并重定向到elf.txt

  1. ELF头

Elf头以一个16进制的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序,ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括ELF头的大小、目标文件的类型、机器类型、字节头部表的文件偏移,以及节头部表中条目的大小和数量等信息。不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的条目。

2、节头部表:包含了文件中出现的各个节的语义,包括节的类型、位置和大小等信息。目标文件中每个节都有一个固定大小的条目。

3、rela.text节:

一个.text节中位置的列表,包含.text节中需要重定位的信息(例如hello.o中的getchar,exit等的重定位信息),需要使用链接器在组合时将这些位置链接。

每一列都包含:

Offset:需要重定向文件在.text或者.data中的偏移量。

Info:包含symbol和type两部分,其中symbol占前4个字节,type占后4个字节,symbol代表重定位目标在symtab中的偏移量,type代表重定位的类型。

Type:重定向的目标类型

Sym.name:重定向到目标名称

Addend:重定向位置的辅助信息

两种最基本的重定位类型:

R_X86_64_PC32 :重定位一个使用32位PC相对地址的引用。

R_X86_64_32 :重定位一个使用32位PC绝对地址的引用。

4、Symbol table是一个符号表,它存放在程序中,用于定义和引用的函数和全局变量的信息。

4.4 Hello.o的结果解析

指令:objdump -d -r hello.o > hello.bjdump objdump -d -r hello.o 

  1. 函数调用时,.s文件中用的是call函数名,而反汇编得到的汇编代码中,是用当前PC+偏移量。
  2. 访问全局变量时,.s文件中使用的是注记符,而反汇编文件中则是.rodata+偏移量。
  3. 分支转移:在反汇编语言中,分支转移时的跳转目标地址为相对偏移量,而原来的.s文件中则是.L2,.L3等注记符。因为转换成机器语言再反汇编之后,注记符不复存在。

4.5 本章小结

汇编器将汇编代码处理成机器码,即二进制代码。二进制代码对人来说可读性较差,但很大程度提升了计算机的执行效率。

汇编代码虽然已经在原来的文本的基础上进行了优化,但是还是存在一些不能直接处理的数据。而在二进制代码中,已经将所有的指令、函数名字等数据变成了对应的存储地址,这样机器就可以直接读取这些代码并执行。我们看到编译之后的汇编将我们的程序向机器又推进了一大步。在汇编中,我们进行了更多的处理,使其变成一个可重定向文件。

5章 链接

5.1 链接的概念与作用

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

作用:链接使得分离编译成为可能,可以将一个大项目分解为较小的、更好管理的模块,可以单独对其进行修改和变异,最后再将其链接到一起。

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.3 可执行目标文件hello的格式

与可重定位文件结构类似。在ELF格式中,节头部表(Section Headers)对hello的各个节的信息做了说明,包括各段的起始地址(Address),大小(size),类型(Type),偏移(Offset)对齐要求(Align)等信息。

指令:linux>readelf -a hello

5.4 hello的虚拟地址空间

在0x00401000段中,程序被载入,即对应的.init, .text, .rodata, .data, .bss节

查看ELF格式文件中的Program Headers, 程序头表在执行时被使用,它告诉链接器加载的内容,并提供动态链接信息,每个表项提供了各段在虚拟地址空间的大小、偏移量,和物理空间的地址、权限标记、对齐长度。

程序包含8个段:

段名   功能

PHDR  保存程序头表

INTERP 程序映射到内存后,调用的解释器

LOAD 程序需要从二进制文件映射到虚拟地址空间的段,保存了常量数据、目标的空间代码等

DYNAMIC 保存动态链接器使用的相关信息

NOTE 存储辅助信息

GNU_STACK 权限标志,标志是否可执行

GNU_RELRO 指定重定位后的哪些区域只需要设置只读

使用edb加载hello,使用data dump查看本进程的虚拟地址空间各段。

例如.interp:

在节头部表中:

在edb中:

       该节的位置,大小都和节头部表一致,其他同理。

5.5 链接的重定位过程分析

指令:linux>objdump -d -r hello

hello与hello.o主要有以下的不同:

  1. 增加的节:hello中增加了.init和.plt节(以及一些节中定义的函数)。
  2. 增加的函数:在hello中链接加入了在hello.c中用到的函数,如exit、printf、sleep等函数。

3.  函数调用:hello中无hello.o中的重定位条目,并且跳转和函数调用的地址在hello中都变成了虚拟内存地址。对于hello.o的反汇编代码,函数只有在链接之后才能确定运行执行的地址,因此在.rela.text节中为其添加了重定位条目。

4.地址访问:hello.o中的相对偏移地址变成了hello中的虚拟内存地址。

根据hello.o和hello的不同分析出链接的过程为:链接器ld将各个目标文件组装在一起,就是把.o文件中的各个函数段按照一定规则累积在一起。

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

5.6 hello的执行流程

hello在执行的过程中共有三大过程,即载入、执行和退出。载入过程的作用是将程序初始化,等初始化完成后,程序才能够开始正常的执行。由于hello程序只有一个main函数,所以在程序执行的时候主要都是在main函数中。又因为main函数中调用了很多其它的库函数,所以可以看到,在main函数执行的过程中,会出现很多其他的函数。

 

 

 

5.7 Hello的动态链接分析

这个过程就是利用GOT表和PLT表之间的交互实现函数的动态链接

之前已经给出过这个GOT里面初始存的是对应PLT表的第二条指令

上图是GOT表中的内容,初始值为PLT表的第二条指令,看一下puts这个函数的第二条指令的地址,的确是这样的。Puts对应的PLT条目的第二条指令是push0,也就是puts函数的ID第三条指令是跳转到PLT表的第一个条目中去,里面是将GOT[1]压入栈中,GOT[1]存放的内容前面已经讲过,是重定位条目:

  

上图是GOT[1]存放的是重定位条目的地址。最后动态链接器根据以上压入栈中的两个参数成功的找到了puts的地址,并写入了puts对应的GOT表

5.8 本章小结

本章阐述了链接器链接hello.o的命令,可执行文件ELF的查看,可执行文件的相关信息,hello的重定位过程和hello的动态链接分析,进一步加深了对链接过程细节的理解。

6章 hello进程管理

6.1 进程的概念与作用

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

作用:进程为用户提供了以下假象:我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存,处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。

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

Shell的作用: Shell 是指一种应用程序,提供了一个界面,用户通过这个界面访问操作系统内核的服务.

处理流程:

1)从终端读入输入的命令。

2)将输入字符串切分获得所有的参数

3)如果是内置命令则立即执行

4)否则调用相应的程序为其分配子进程并运行

5)shell应该接受键盘输入信号,并对这些信号进行相应处理

6.3 Hello的fork进程创建过程

Shell(父进程)通过fork 函数创建一个新的运行的子进程。新的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。父进程和新创建的子进程之间最大的区别在于它们有不同的PID。父进程和子进程是并发运行的独立进程,内核能够以任何方式交替执行它们的逻辑控制流中的指令。

子进程进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。

6.4 Hello的execve过程

fork创建子进程后,在shell创建的子进程中将会调用execve函数并带参数列表argv和环境变量envp,来调用加载器,加载器将可执行目标文件中的代码和数据从磁盘复制到内存中,覆盖当前进程的代码、数据、栈,但是保留PID,继承已打开的文件描述符和信号上下文,然后通过跳转到程序的第一条指令或入口点来运行该程序。

每个程序都有一个运行时内存映像。当加载器运行时,在程序头部表的引导下,加载器将可执行文件的片(chunk)复制到代码段和数据段。接下来,加载器跳转到程序的入口点,也就是_start函数的地址。这个函数是在系统目标文件ctrl.o中定义的,对所有的C程序都是一样的。_start函数调用系统启动函数_ _libc_start_main,该函数定义在libc.so中,它初始化执行环境,调用用户层的main函数,处理main函数的返回值,并且在需要的时候把控制返回给内核。

6.5 Hello的进程执行

在Hello执行的某些时刻,比如sleep函数,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度。当内核调度了一个新的进程运行后,它就抢占Hello进程,并且使用上下文切换机制来将控制转移到新的进程。

Hello进程初始运行在用户模式中,直到Hello进程中的sleep系统调用,它显式地请求让Hello进程休眠,内核可以决定执行上下文切换,进入到内核模式。当定时器2.5后中断时,内核就能判断当前Hello休眠运行了足够长的时间,切换回用户模式。

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

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

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

6.6 hello的异常与信号处理

Hello在执行的过程中,可能会出现处理器外部I/O设备引起的异常,执行指令导致的陷阱、故障和终止。第一种被称为外部异常,常见的有时钟中断、外部设备的I/O中断等。第二种被称为同步异常。陷阱指的是有意的执行指令的结果,故障是非有意的可能被修复的结果,而终止是非故意的不可修复的致命错误。

在发生异常时会产生信号。例如缺页故障会导致OS发生SIGSEGV信号给用户进程,而用户进程以段错误退出。常见信号种类如下表所示。

ID  名称      默认行为 相应事件

2    SIGINT    终止      来自键盘的中断

9   SIGKILL   终止      杀死程序(该信号不能被捕获不能被忽略)

11  SIGSEGV  终止      无效的内存引用(段故障)

14  SIGALRM  终止    来自alarm函数的定时器信号

17  SIGCHLD  忽略      一个子进程停止或者终止

键入Ctrl-C,会向当前进程发送一个SIGINT信号,从而使当前进程中断。

键入回车将会被忽略。

键入Ctrl-Z,会发送一个 SIGTSTP 信号给前台进程组中的进程,从而将其挂起,

键入Ctrl-z后可以键入ps查看进程及运行时间;可以键入jobs查看当前暂停的进程;可以键入fg使进程在前台执行;也可以键入kill杀死特定进程。

6.7本章小结

本章介绍了进程的定义和功能,shell的处理流程,shell如何调用fork来创建新进程,如何调用execve来执行Hello,分析了Hello的进程执行,并研究了Hello的异常和信号处理。

异常控制流发生在计算机系统的所有层。在硬件层,硬件检测到的事件会触发控制权突然转移到异常处理程序。在操作系统级别,内核通过上下文切换将控制权从一个用户进程转移到另一个用户进程。在应用层,一个进程可以向另一个进程发送信号,而接收器会突然将控制权转移到它的一个信号处理程序。程序可以通过避免常见的堆栈规则和执行非本机跳转到其他函数中的任何位置来对错误作出反应。

7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:一个逻辑地址,是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 [段标识符:段内偏移量]。

线性地址&虚拟地址:某地址空间中的地址是连续的非负整数时,该地址空间中的地址被称为线性地址。如果启用了分页机制,那么线性地址能再经变换以产生一个物理地址。若没有启用分页机制,那么线性地址直接就是物理地址。Intel 80386的线性地址空间容量为4G(2的32次方即32根地址总线寻址)。

物理地址:计算机主存被组织成由M个连续字节大小的内存组成的数组,每个字节都有一个唯一的地址,该地址被称为物理地址。h是地址变换的最终结果地址。如果启用了分页机制,那么线性地址会使用页目录和页表中的项变换成物理地址。如果没有启用分页机制,那么线性地址就直接成为物理地址了。

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

逻辑地址由两部分组成:段标识符和段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节。通过一个索引,可以定位到段描述符,进而通过段描述符得到段基址。段基址与偏移量结合就得到了线性地址,虚拟地址。

索引:用来确定当前使用的段描述符在描述符表中的位置;

TI:根据TI的值判断选择全局描述符表(TI=0,GDT)或选择局部描述符表(TI=1,LDT);

RPL:判断重要等级。RPL=00,为第0级,位于最高级的内核,RPL=11,为第3级,位于最低级的用户状态;

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

CPU的页式内存管理单元负责把一个线性地址转换为物理地址。线性地址被划分成固定长度单位的数组,称为页(page)。例如,一个32位的机器,线性地址可以达到4G,用4KB为一个页来划分,这样,整个线性地址就被划分为一个2^20次方的的大数组,共有2的20次方个页,也就是1M个页,这个大数组我们称之为页目录。

类似地,物理内存也被分割为物理页,分页单元将所有的物理内存都划分成了固定大小的单元为管理单位,其大小一般与内存页大小一致。

页表的根本功能是提供从虚拟页面到物理页面的映射,改页表中每一项存储的都是物理页的基地址。

当页面命中时CPU硬件执行的步骤:

第1步:处理器生成一个虚拟地址(VA),并把它传送给MMU。

第2步::MMU生成PTE地址,并从高速缓存/主存请求得到它。

第3步:高速缓存/主存向MMU返回PTE。

第4步:MMU构造物理地址(PA),并把它传送给高速缓存/主存。

第5步:高速缓存/主存返回所请求的数据字给处理器。

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

虚拟地址是由VPN和VPO组成的,VPN可以作为在TLB中的索引,TLB可以看作是一个PTE的cache,将常用的PTE缓存到TLB中,加速虚拟地址的翻译。TLB是具有高相连度的。如果能够在TLB中找到与VPN对应的PTE,即TLB hit,TLB直接给出PPN,PPO即为VPO,这样就构成了一个物理地址。

如果不能做到TLB hit就要到四级页表当中取寻址,在i7中VPN有36位,被分成了四段,从左往右的前三个九位的地址分别对应于在前三级页表当中的偏移,偏移在页表中所对应的页表条目指向某一个下一级页表,而下一个9位VPN就对应的是在这个页表中的偏移。最后一级页表中的页表条目存放的是PPN

最后再把VPO拿来当成PPO就能找到在对应的物理页上存放的内容了。

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

MMU发送物理地址PA给L1缓存,L1缓存从物理地址中抽取出缓存偏移CO、缓存组索引CI以及缓存标记CT。高速缓存根据CI找到缓存中的一组,并通过CT判断是否已经缓存地址对应的数据,若缓存命中,则根据偏移量直接从缓存中读取数据并返回;若缓存不命中,则继续从L2、L3缓存中查询,若仍未命中,则从主存中读取数据。

7.6 hello进程fork时的内存映射

当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID,为了给这个新进程创建虚拟内存,他创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制,当fork函数在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的概念。

7.7 hello进程execve时的内存映射

exceve函数加载和执行程序Hello步骤:

1.删除已存在的用户区域。

2.创建私有区域。为Hello的代码、数据、bss和栈区域创建新的区域结构。

3.创建共享区域。比如Hello程序与标准C库libc.so链接,这些对象都是动态链接到Hello,映射到用户虚拟地址空间的共享区域中。

4.设置程序计数器(PC),使之指向代码区域的入口点。

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

第一步确认是不是一个合法的地址,即通过不断将这个地址与每个区域的vm_start&vm_end进行比对,如果并不是在一个区域里的话,就给出segmentation fault,因为它引用了一个不合法的地址

第二步确认访问权限是不是正确的。即如果这一页是只读页,但是却要做出写这个动作,那明显是不行的。如果做出了这类动作,那么处理程序就会触发一个保护异常,默认行为是结束这个进程

第三步确认了是合法地址并且是符合权限的访问,那么就用某个特定算法选出一个牺牲页,如果该页被修改了,就将此页滑出(swap out)并且swap in那个被访问的页,并将控制传递到触发缺页中断的那条指令,这条指令继续执行的时候就不会触发缺页中断,这样就可以继续执行下去。

7.9动态存储分配管理

隐式空闲链表

隐式空闲链表在每个空闲块中给出了四个字节的头部header,因为地址一定是8字节对齐的,所以最后三位肯定是空的,所以最后三位中的1位可以用来存放这个块是否分配(0/1)的信息。还可以加入一个脚部footer,在合并时可以用来确定前一个块是不是空的,因此最小块大小是8个字节,而将空闲链表分配出去时可以不用加入footer,只需在header的空闲3位中再选1位用来作为前一块是否是空闲的标记(0/1)就可以了。

隐式链表的好处是简单,易于操作,但坏处在于搜索时间太长,如果采用首次适配算法的话内存利用率会低,但如果采用最佳适配的话需要对于一整个堆进行搜索。

显式空间链表

对于分配的块,只需要一个header和一个footer来表示已经分配过了以及标明大小方便回收的时候插入空闲链表。而对于未分配的块,需要分配一个指向前面一个块的指针和指向后面一个块的指针来显示地组成这个链表。如果使用后进先出的方法那么所有的free操作都可以在常数时间内完成,如果都用了footer进行标记的话合并操作也可以在常数时间内完成。如果使用的是地址从小到大排列的话那么内存利用率会上升,但相应的要付出更加长的搜索时间的代价。显式空闲链表的坏处就是会增加内部碎片。

分离链表:

分离链表分:简单分离链表、分离适配和伙伴系统。简单分离链表的每个等价类中的块大小是一样的,所以可以根据地址判断块大小,分配可以在常数时间内完成。因为块大小固定,所以不需要合并,只需要寻找不同的等价类就可以了,那么header和footer也不需要了,只需要一个能够指向后面块的指针就可以了,这样会大降低内存开销,但是一个很大很大的缺陷就是这样会产生很大很大很大的内部和外部碎片

分离适配是每个等价类中的块大小不尽相同,而每个等价类代表的是一定的大小范围的块,这样做的好处是可以在堆的某一块中搜索从而降低搜索时间,并且有研究证明分离适配的首次适配大约就等于别的空闲链表的最佳适配,内存利用率显著上升。

伙伴系统是对于堆一开始是一整块,然后每次申请空间都会向上舍入到离它最近的2得幂次方字节,通过对现有大小的块进行不断二分直到找到符合舍入后大小的块为止。坏处是它向上舍入到2的幂次方字节会产生很大的内部碎片,也有可能产生外部碎片,因此这不适合通用的内存分配,但是一旦我们初始知道分配大小为2的整数次幂的话就非常具有吸引力了,因为分配和合并都非常快。

7.10本章小结

本章阐述了虚拟地址、线性地址、物理地址的区别,通过虚拟地址到物理地址的转换,进一步加深了对虚拟地址空间及其强大作用的理解。我们讨论了 hello 的存储器地址空间、intel 的段式管理、hello 的页式管理, 以i7为例介绍了VA 到PA 的变换、物理内存访问,还介绍了hello 进程 fork 时的内存映射、execve 时的内存映射、缺页故障与缺页中断处理、动态 存储分配管理。同时还体会了动态内存管理时,申请、分割、合并、回收等具体过程,加深了我们对动态内存管理过程的理解与认识。

8章 hello的IO管理

8.1 Linux的IO设备管理方法

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

设备管理:Linux内核的简单的低级接口--unix io接口

8.2 简述Unix IO接口及其函数

Unix IO接口:

打开文件,内核返回一个非负整数的文件描述符,通过对此文件描述符对文件进行所有操作。

Linux shell创建的每个进程开始时都有三个打开的文件:标准输入(文件描述符0)、标准输出(描述符为1),标准出错(描述符为2)。头文件<unistd.h>定义了常量STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO,他们可用来代替显式的描述符值。

改变当前的文件位置,文件开始位置为文件偏移量,应用程序通过seek操作,可设置文件的当前位置为k。

读写文件,读操作:从文件复制n个字节到内存,从当前文件位置k开始,然后将k增加到k+n;写操作:从内存复制n个字节到文件,当前文件位置为k,然后更新k

关闭文件。当应用完成对文件的访问后,通知内核关闭这个文件。内核会释放文件打开时创建的数据结构,将描述符恢复到描述符池中

Unix IO函数:

read和write–最简单的读写函数;

readn和writen–原子性读写操作;

recvfrom和sendto–增加了目标地址和地址结构长度的参数;

recv和send–允许从进程到内核传递标志;

readv和writev–允许指定往其中输入数据或从其中输出数据的缓冲区;

recvmsg和sendmsg–结合了其他IO函数的所有特性,并具备接受和发送辅助数据的能力。

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;

}

(char*)(&fmt)+4)表示的是可变参数中的第一个参数的地址。vsprintf的作用是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。接着从vsprintf生成显示信息到write系统函数,直到陷阱系统调用int 0x80或syscall。字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

getchar函数的大致实现如下:

代码:

int getchar(void)
{

char c;

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

}

getchar由宏实现:#define getchar() getc(stdin)。getchar有一个int型的返回值。当程序调用getchar时程序等待用户按键。用户输入的字符被存放在键盘缓冲区中,直到按回车为止(回车字符也放在缓冲区中)。用户键入回车之后,getchar开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的字符的ASCII码,文件结尾(End-Of-File)则返回-1(EOF),且将用户输入的字符回显到屏幕。

若用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。直到缓冲区中的字符读完后,才等待用户按键。

getchar函数通过调用read函数来读取字符。read函数由三个参数,第一个参数为文件描述符fd,fd为0表示标准输入;第二个参数为输入内容的指针;第三个参数为读入字符的个数。read函数的返回值是读入字符的个数,若出错则返回-1。

8.5本章小结

本章阐述了linux下IO设备的管理方法,了解了Unix IO接口及函数,分析了printf函数和getchar函数的实现。

结论

Hello完整的一生:

1.编写阶段:通过文本编辑器写出hello.c。

2.预处理阶段:预处理器将hello.c进行字符替换, #include的外部的库取出合并到hello.i文件中。

3.编译阶段:文本文件hello.i翻译成文本文件hello.s。

4.汇编:将.s汇编程序翻译成机器语言指令,hello.s会变成为可重定位目标文件hello.o。

5.链接:将hello.o与可重定位目标文件和动态链接库链接成为可执行目标程序hello。

6.运行:在shell中输入./hello

7.运行程序:shell进程调用fork创建子进程,shell调用execve,execve调用启动加载器,加映射虚拟内存,创建新的内存区域,并创建一组新的代码、数据、堆和栈段。程序开始运行。

8.执行指令:CPU为其分配时间片。在一个时间片中,hello有对CPU的控制权,从而顺序执行自己的代码。

10.访问内存:MMU将程序中使用的虚拟内存地址通过页表映射成物理地址。

11.动态申请内存:printf会调用malloc向动态内存分配器申请堆中的内存。

12.信号:如果运行途中键入ctr-c则停止,如果运行途中键入ctr-z则挂起。

13.结束:shell父进程回收子进程。

感悟与创新:现代计算机系统设计复杂而巧妙,计算机系统的各个部件和设计思想都有其重要作用,配合紧密巧妙。我们在写程序时,也应该考虑到系统层面的程序执行效率因素,提升程序效率。计算机系统从图灵机的基础上发展到今天,已经形成了一个非常成熟的系统,有很多重要的设计理念,比如局部性原理。未来计算机的发展是否能与量子结合继续提高硬件能力,是否会相应地产生新的设计理念和算法,有待我们进一步探索。

附件

hello.i 预处理产生的文本文件

hello.s 编译产生的汇编文件

hello.o 汇编产生的可重定位目标文件

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

helloo.objdmp Hello.o的反汇编代码

helloo.elf Hello.o的ELF格式

hello.objdmp Hello的反汇编代码

hello.elf Hello的ELF格式

参考文献

[1]  林来兴. 空间控制技术[M]. 北京:中国宇航出版社,1992:25-42.

[2]  辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.

[3]  赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).

[4]  谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.

[5]  KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.

[6]  CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.

[7] 《深入理解计算机系统》Randal E.Bryant,David R.O Hallron

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值