HIT CSAPP:程序人生-Hello‘s P2P

诗林  金牌会员 | 2024-6-15 00:37:03 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 351|帖子 351|积分 1053


                                                                          摘  要
多年以后,面临着黑暗的控制台,程序员将会想起控制台上打印出“Hello World”的谁人遥远的下午。那时间的程序员青涩鸠拙,谁人寻常的字符串就此成为了他的初恋,开启了他的编程之旅。之后他所写的代码愈来愈纷繁复杂,遇到的bug也愈来愈棘手,他也成为了饱经考验的程序员。然而终究有一天,他会回到之前的起点,意识到“Hello World”显如今控制台上背后所蕴含的,巨大深邃的智慧英华。
为了深入了解计算机体系在程序从无到有到实行到停止的过程中的所作所为,本文围绕简单的C语言程序hello灿烂辉煌的一生,对hello程序“From Program to Process”和“From Zero-0 to Zero-0”两个过程展开分析,回首了其在计算机体系中完备的生命过程:经过预处理、编译、汇编、链接最终成为了可以实行的文件hello,然后在CS中完成被folk成为进程,被execve加载,经过对控制流的管理,内存空间的分配,异常的处理,对I/O设备的调用,最终被shell接纳。笔者借此深入明白计算机体系各个部门运作的方式与机制,较为完备地揭示x86-64计算机体系田主要工作机制以及顶层程序员与底层机器的沟通逻辑与原理,领略到了计算机体系冰山一角的风采。

关键词:计算机体系;操纵体系;进程;虚拟内存;编译;信号;I/O管理;                          







目  录


第1章 概述

1.1 Hello简介

1.1.1. P2P:From Program to Process

1.1.2. 020From Zero-0 to Zero-0

1.2 环境与工具

1.3 中间结果

1.4 本章小结

第2章 预处理

2.1 预处理的概念与作用

2.1.1.预处理的概念

2.1.2.预处理的作用

2.2在Ubuntu下预处理的命令

2.3 Hello的预处理结果分析

2.4 本章小结

第3章 编译

3.1 编译的概念与作用

3.1.2.编译的作用

3.2 在Ubuntu下编译的命令

3.3 Hello的编译结果分析

3.3.1.变量

3.3.2.常量数据和赋值

3.3.3.if分支语句

3.3.4.for循环

3.3.5. 函数操纵

3.4 本章小结

第4章 汇编

4.1 汇编的概念与作用

4.1.1.汇编的概念

4.1.2.汇编的作用

4.2 在Ubuntu下汇编的命令

4.3 可重定位目标elf格式

4.3.1.格式图片与readelf命令

4.3.2. hello.o的elf头

4.3.3. hello.o的节头表

4.3.4. hello.o的重定位节

4.3.5. hello.o的符号表

4.4 Hello.o的结果分析

4.5 本章小结

5链接

5.1 链接的概念与作用

5.1.1.链接的概念

5.1.2.链接的作用

5.2 在Ubuntu下链接的命令

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

5.3.1. hello的ELF头

5.3.2. hello的节头表

5.3.3. hello的程序头表

5.4 hello的虚拟地点空间

5.5 链接的重定位过程分析

5.5.1.新增函数

5.5.2.新增节

5.5.3.函数调用地点

5.5.4.控制流跳转地点

5.6 hello的实行流程

5.7 Hello的动态链接分析

5.8 本章小结

6hello进程管理

6.1 进程的概念与作用

6.1.1.进程的概念

6.1.2.进程的作用

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

6.3 Hello的fork进程创建过程

6.4 Hello的execve过程

6.5 Hello的进程实行

6.5.1.逻辑控制流

6.5.2.进程时间片

6.5.3.用户与内核模式

6.5.4.进程的上下文切换

6.6 hello的异常与信号处理

6.6.1.正常实行

6.6.2.乱按键盘

6.6.3.Ctrl+C

6.6.4.Ctrl+Z

6.7本章小结

7hello的存储管理

7.1 hello的存储器地点空间

7.1.1.逻辑地点

7.1.2. 线性地点

7.1.3.虚拟地点

7.1.4. 物理地点

7.2 Intel逻辑地点到线性地点的变动-段式管理

7.3 Hello的线性地点到物理地点的变动-页式管理

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

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

7.6 hello进程fork时的内存映射

7.7 hello进程execve时的内存映射

7.8 缺页故障与缺页停止处理

7.9动态存储分配管理

7.10本章小结

8hello的IO管理

8.1 Linux的IO设备管理方法

8.2 简述Unix IO接口及其函数

8.2.1.Unix I/O接口

8.3 printf的实现分析

8.4 getchar的实现分析

8.5本章小结

结论

附件

参考文献



第1章 概述

1.1 Hello简介




      • P2P:From Program to Process


即从源文件到可实行目标文件的转化。在Unix体系上,将用高级语言编写的hello.c文件转换为可实行目标文件hello的过程始于GCC编译器的读取。这个过程包括以下四个主要阶段:
(1)预处理阶段:预处理器(cpp)读取源文件hello.c并对其举行修改,天生一个经过预处理的文本文件hello.i;
(2)编译阶段:编译器(ccl)接收hello.i文件,将其翻译成相应的汇编程序hello.s;
(3)汇编阶段:汇编器(as)接收汇编程序hello.s,将其翻译成机器语言指令,并将这些指令打包成可重定位目标程序hello.o;
(4)链接阶段:链接器(ld)将hello.o与所需的库函数链接,最终天生可实行目标程序hello;
实行该程序时,操纵体系(CS)通过fork体系调用创建一个子进程来运行hello程序,至此,hello.c从一个程序变化为一个进程。



      • 020From Zero-0 to Zero-0


即进程从无到有的产生以及,最终被接纳消失再化为无的过程。Shell通过execve函数加载并运行可实行目标文件hello。在此过程中,操纵体系为hello分配虚拟内存地点VA,并在物理内存地点PA与虚拟内存地点VA之间建立映射关系。虚拟内存为进程提供独立的内存空间,而TLB、四级页表、三级缓存以及Pagefile文件的存在保证了数据从磁盘到CPU的高效传输,同时I/O管理和信号处理协同工作以实现hello的输入输出功能。当程序运行结束后,父进程负责接纳hello进程,并释放相应的虚拟内存空间。于是,hello进程完成了从无(0)到有再到无(0)的整个生命周期过程。
1.2 环境与工具

1.2.1.硬件环境
处理器:AMD Ryzen 9 6900HX; RAM:32.00GB
1.2.2.软件环境
Windows 11 64位,VMware Workstation Pro,Ubuntu 22.04.03 LTS 64位
1.2.3.开辟与调试工具
gcc/ as /ld /Vim /Edb /readelf /gedit /Visual Studio /CodeBlocks
1.3 中间结果


  • 1. hello.c:源程序文件
  • 2. hello.i:hello.c预处理后的源程序文件
  • 3. hello.s:hello.i编译后的汇编程序
  • 4. hello.o:hello.s汇编后的可重定位目标文件
  • 5. elf.txt:readelf检察hello.o的结果,hello.o的ELF文件
  • 6. dump_hello.txt:hello.o的反汇编代码
  • 7. hello:hello.o链接后的可实行目标文件(不包含可调试选项)
  • 8. hello1:hello.o链接后的可实行目标文件(包含可调试选项)
  • 9. hello_objdump.s:hello的反汇编代码
1.4 本章小结

本章枚举出了实行所利用的硬件环境、软件环境以及编译调试工具,枚举出了天生的各个中间文件的名称。通过简述hello的完成生命过程,简单阐释了从编写、预处理、编译、汇编、链接形成进程以及该进程从无到有再到进程结束后被接纳的各个阶段。一个关于hello程序的故事,即将上演。


第2章 预处理

2.1 预处理的概念与作用

2.1.1.预处理的概念
预处理是编译器在实际编译之前实行的一个关键步骤,由C预处理器(C Preprocessor)负责完成。预处理器cpp根据源代码中的预处理指令等信息对源代码文件(文本文件)举行修改,将其替换成为新的程序文件(通常是以.i作为文件扩展名)。
在预处理过程中,预处理器会根据预处理指令(如#include、#define、#ifndef、#endif等)来处理源程序的文本内容。
学者们将预处理指令主要分为三类:“文件包含”、“宏替换”和“条件编译”[1]。文件包含:在编写C语言代码时,通常必要利用#include指令来引入头文件,预处理器会将被引入文件的源代码插入到被处理程序的相应位置;宏替换:通过#define指令,我们可以将一个标识符(宏名)替换为一个字符串。预处理过程中,必要将程序中利用的宏名展开为其对应的字符串;条件编译:根据#ifndef、#endif、#if、#elif等预处理指令限定的条件,预处理器会保留满意条件的代码部门,同时移除不满意条件的代码部门。(注:学者们关于宏替换和宏展开似乎说法不一,笔者在此处并未深究)。
例如读取头文件<stdio.h>的内容,而且把它直接插入到程序文本中,对#define定义的宏举行替换,根据条件选择#if内的代码等。
2.1.2.预处理的作用
预处理实现了在编译器举行编译之前对源代码做针对预处理指令的转换。例如宏替换(将宏定义的名称替换为字符串或数值),文件包含(读取头文件中的内容并将其直接插入到程序文本中),条件编译(根据条件编译指令决定必要编译的代码),删除注释等功能。
预处理指令通常用于方便地调整或修改源代码以适应不同的实行环境。预处理的作用在于为编译器的工作做好准备。起首,它通过#include指令将头文件中的代码归并到被处理的程序中,确保后续的编译过程可以大概顺利举行,从而显著淘汰了程序员的工作量。其次,宏定义和宏替换机制在保证程序精确编译的同时,通过利用宏名来代替一段字符串,使程序员可以大概更轻松地编写代码,提高了代码的简便性和可读性。别的,条件编译机制有效地剔除了一些无用代码,淘汰了编译过程的负担。通过奇妙地运用条件编译,还可以避免头文件的重复包含,并提升程序的可移植性,使其可以大概在不同的体系中举行移植。
2.2在Ubuntu下预处理的命令

预处理命令:gcc -m64 -no-pie -fno-stack-protector -fno-PIC -E hello.c -o hello.i

图-2-1-在Ubuntu下的预处理过程

2.3 Hello的预处理结果分析

通过文本编辑器分别比较预处理后的文件和C语言源文件,我们可以观察到以下几个特点:

(1)原始源文件仅有24行,预处理后的文件有3091行。预处理后的文件体积大为增长。别的,我们可以观察到预处理后的文件中不再包含#include指令,这表明预处理过程确实将#include指令引入的文件内容嵌入到了源程序中;
(2)在预处理后的文件中,我们的源程序中的注释全部消失了,这表明预处理过程会删除源程序中的注释内容;
(3)分析两个文件中main函数的内容,由于我们没有利用#define举行宏定义,也没有在main函数内部利用条件编译的预处理指令,因此main函数内部的内容没有发生厘革,纵然是第13行的空行也被保留了下来。

因此,可以得出一个简单的结论:在预处理过程中,除了实行宏替换、文 件包含、条件编译以及删除注释等操纵,不会举行其他多余的处理。进一步观 察预处理后的hello.i文件的开头部门,我们可以发现:
开头主要是头文件中的声明内容,这些声明被复制到了hello.i文件中。别的尚有一些以#开头的.h文件地点。
同时我们留意到原始头文件中的#ifndef、#endif等预处理指令在预处理后的文件中消失了,这表明预处理过程不仅对我们的源程序举行处理,同时也对包含的头文件举行了处理。因此,在头文件中参加一些条件编译指令可以有效防止头文件被重复包含的环境。

图-2-2-hello.i main函数部门代码截图

2.4 本章小结

本章具体阐述了hello.c通过预处理器(cpp)变化为hello.i文件的过程,并简要先容了预处理的概念。对经过预处理后的文件举行了简要分析:预处理器实际上对源代码举行了广泛的展开,扩展了原有的#include指令所包含的头文件,并替换了一些宏定义。
预处理过程本质上是对文本举行增加、删除和替换的操纵。只有经过预处理的C语言源程序才能被编译器精确编译及处理后续步骤。总的来说,预处理是C语言源程序朝着天生可实行文件迈出的简单却又重要的第一步。


第3章 编译


3.1 编译的概念与作用

3.1.1.编译的概念
在袁春风老师编写的《计算机体系基础》[2]中,编译过程被描述为以下方式:“C编译器在举行具体的程序翻译之前,会先对源程序举行词法分析和语法分析,然后根据分析的结果举行代码优化和存储分配,最终把C语言源程序翻译成汇编语言程序。”简言之,编译是指将预处理后的.i文件转换为汇编语言程序.s文件的过程,这由C编译器(ccl)完成。
编译器(ccl)将高级语言转化为低级语言的翻译过程可分为以下5个阶段:词法分析、语法分析、语义检查和中间代码天生、代码优化、目标代码天生。此中,词法分析和语法分析(即源程序分析)最为重要。如果在分析过程中发现语法错误,编译器将提供相应的提示信息。
3.1.2.编译的作用
编译是将经过预处理后的.i文件转换成汇编语言程序.s文件的过程,其主要目标与前述的翻译过程相关,特别是将人类熟悉的、更符合人类逻辑习惯的高级语言转换成更接近计算机的低级语言。如果将此过程展开来看,可以将其主要分为两个部门:分析部门和综合部门[3]。
分析部门将源程序分解为多个组成要素,并为这些要素添加语法布局。然后,利用这个布局创建源程序的中间表示。同时,分析部门会对源程序的语法举行检查。如果发现源程序不符合精确的语法构成,大概在语义上存在不划一之处,分析部门将提供有用的信息(这类似于我们在编译时遇到的错误和告诫)。别的,分析部门还会收集有关源程序的信息,并将其存储在称为符号表的数据布局中,然后将符号表与中间表示情势一起传递给综合部门。
综合部门根据中间表示和符号表中的信息构建用户期望的目标程序。
图3-1为《编译原理》一书中所给出的编译器的流程图。

图-3-1 编译器的各个步骤

其各个过程的作用如下:
1.语法分析词法分析:编译程序的语法分析器以单词符号作为输入,分析单词符号串是否形成符合语法规则的语法单位,方法分为两种:自上而下分析法和自下而上分析法。
2.中间代码:源程序的一种内部表示,或称中间语言。中间代码的作用是可使编译程序的布局在逻辑上更为简单明确,特别是可使目标代码的优化比较容易实现中间代码。
3.代码优化:指对程序举行多种等价变动,使得从变动后的程序出发,能天生更有效的目标代码。
4.目标代码:天生是编译的最后一个阶段。目标代码天生器把语法分析后或优化后的中间代码变动成目标代码。此处指汇编语言代码,须经过汇编程序汇编后,成为可实行的机器语言代码
3.2 在Ubuntu下编译的命令

编译命令:gcc -m64 -no-pie -fno-stack-protector -fno-PIC -S hello.i -o hello.s


图-3-2在Ubuntu下的编译过程

3.3 Hello的编译结果分析

3.3.1.变量
全局变量:
已经初始化而且初始值非零的全局变量储存在.data节,未初始化的全局变量存储在.bss节在加载进入内存时释放空间,它的初始化不必要汇编语句,而是通过虚拟内存哀求二进制零的页直接完成的。
局部变量:
在hello的源代码中,并没有全局变量,而是包含了几个局部变量:函数 参数argc和argv,以及for循环中的i。此中,argc表示参数argv的个数。这 些变量都存储在栈中。具体地说,argc的地点为-20(%rbp),argv的首地点为-32 (%rbp),而每个参数占用8个字节(在Linux 64位体系中,每个指针占8字节), 因此,第k个参数的地点为-(32+8k)(%rbp),而i的地点为-4(%rbp)。
hello.c源码与汇编代码的对比如图3-3所示:

图-3-3 hello.i(左)与hello.s局部变量代码对照

3.3.2.常量数据和赋值
常量数据:
在hello.c的main函数中,常量包括printf函数中打印的两个字符串常量和if条件、for循环里的数字常量。此中如图6,所示字符串常量被存储在./rotate段,如图3-5所示,数字常量被存储在.text段中,且作为立即数出现:

图-3-4在hello.s中的字符串常量


图-3-5 hello.s中if与for中的数字常量

赋值:
如图3-3左上及右边白色方框所示,右边的hello.c源码中,对与局部变量i举行赋值初始化,即:将0赋给i,在汇编代码中对应第一行“movl $0, -4(%rbp)故在汇编指令中利用movl x y 的情势举行直接赋值操纵。
3.3.3.if分支语句
在hello.c文件中存在一段if分支语句“if(argc!=5)”。在hello.s中,这段语句的实现如图8所示。根据hello.c文件的逻辑,edi寄存器中存储了main函数的参数argc的值,因此,通过cmpl指令来比较edi寄存器的值与常量5的值(此时常量5位于.text节)。
具体的实行流程如下:cmpl指令将edi寄存器的值与立即数5举行比较(实际上计算%edi-5),如果结果等于0(相等),则将ZF标记位设置为1;如果结果不等于0(不相等),则将ZF标记位设置为0。根据ZF标记位的值举行条件跳转,从而实现了if语句的条件分支。根据je .L2指令,当ZF=1时,即argc等于5时,将实行.L2标签内的代码。
至于操纵符!=的判定方式,如上所述,通过cmpl指令对这两个数举行比较,如果将ZF标记位设置为0,则表示这两个数不相等。

图-3-6if分支语句的汇编实现

3.3.4.for循环
在hello.c中有一段for循环语句,下面我们将从各个部门分别分析for循环在汇编语言中的实现。
for循环的循环体是由图3-7所示的.L4标签内的代码实现的。

图-3-7 for循环的循环体


如前文所述,for循环的初始条件(i=0),是由movl指令,将表示局部变量i的寄存器ebp的值直接赋为0。此时赋值操纵符“=”的实现就是movl语句的数据传送。
对于for循环的判定条件(i<10),当i小于10时,程序实行for循环的循环体。在汇编语句中,这种判定条件的实现(如图10所示)与之前if分支的实现相似。通过cmpl指令比较寄存器ebp的值和常量9(此时常量9也位于.text节),cmpl根据%ebp-9的结果修改多个标记的值。如果ebp的值小于9,cmpl将OF标记设为1,将SF标记设为1;如果ebp的值等于9,cmpl将ZF标记设为1。由于jle条件跳转的跳转条件为(SF^OF|ZF),因此当ebp的值小于或等于9时,jle的跳转条件建立,从而实行for循环的循环体(.L4内的代码)。
  

图-3-8 for循环的判定条件

对于for循环的迭代(i++),每次循环实行完毕后都会举行i++的迭代操纵。迭代操纵如图11所示。在循环体实行完毕后,通过addl指令实现寄存器ebp的值加1(即i++),然后继续实行.L3内的代码,判定ebp的值是否小于等于9,如果是则继续实行循环体(.L4内的代码),否则实行之后的语句,跳出循环。
        

     图-3-9 for循环的迭代操纵

3.3.5. 函数操纵
在hello.i文件中,涉及到了5个函数调用,分别是main主函数、printf、exit、sleep和getchar。对于Linux X86-64体系,函数调用传递参数的规则如下:前6个参数依次存储在%rdi、%rsi、%rdx、%rcx、%r8和%r9这六个寄存器中,多余的参数则保存在栈中。在调用函数之前,会利用mov指令,按照参数逐个从后向前的次序加载到寄存器或栈中。值得留意的是,一些典型的环境,比如调用atoi和sleep函数,会将寄存器%rax中的值传送到存储第一个参数的寄存器%rdi中;而像getchar这样没有参数的函数,则无需举行这样的操纵。对于main函数的返回(return 0),在汇编代码中通常利用以下指令来实现:起首利用“movl $0, %eax”将0传送到%eax寄存器,然后通过“leave”指令来恢复调用者(caller)的栈帧,清理被调用者(callee)的栈帧,最后利用“ret”指令返回,即找到下一条指令的地点。
 


图-3-10 hello.s中的函数调用与参数传递

3.4 本章小结

本章具体先容了编译的概念和过程。通过在Ubuntu下利用编译指令,我们完成了对hello.i的编译工作,将其文本文件转换为了hello.s的汇编语言文件。别的,通过将天生的汇编语言指令与源文件C程序代码举行比较,我们分析了汇编代码,阐述了其怎样实现全局与局部变量、常量数据与赋值、关系操纵与控制转移以及函数调用等过程。完成了这一阶段的转换后,hello进入了下一个阶段,即汇编处理。

第4章 汇编


4.1 汇编的概念与作用

4.1.1.汇编的概念
汇编是将编译后产生的.s汇编文件翻译成机器语言指令的过程,这一过程由汇编器(as)完成。在经过编译器(ccl)处理后,hello.i源程序被送至汇编器(as)。汇编器会将汇编语言的ASCII码文件(如今是hello.s)转换为机器语言指令,并将其打包成可重定位目标文件格式(hello.o)。这个过程被称为汇编。
4.1.2.汇编的作用
汇编的作用是将汇编语言代码转换成机器语言代码,并将结果保存在可重定位目标文件(.o二进制文件)中。可重定位目标文件(hello.o)是一个二进制文件,内含程序的二进制指令编码。汇编器(as)将.s汇编程序转换成机器语言指令,并以可重定位目标文件的格式保存这些二进制指令编码在.o目标文件中,以便用于后续的链接。
4.2 在Ubuntu下汇编的命令

汇编命令:gcc -m64 -no-pie -fno-stack-protector -fno-PIC -c hello.s -o hello.o
也可以利用as汇编器直接汇编:as hello.s -o hello.o

图-4-1 在Ubuntu下的汇编过程

4.3 可重定位目标elf格式

4.3.1.格式图片与readelf命令
如图4-2所示,在《深入明白计算机体系》一书中描述了ELF可重定位目 标文件的典型格式,利用命令:readelf -a hello.o > ./elf.txt 来检察hello.o的elf 文件;
可重定位目标elf格式中包含各个节以及其所含信息如下:

图-4-2 可重定位目标文件elf格式各节内容


4.3.2. hello.o的elf头
起首是elf头,如图4-3所示包括16字节标识信息、文件类型、机器类型、节头表偏移、节头表的表项大小以及表项个数。

图-4-3 可重定位格式目标文件elf头 


4.3.3. hello.o的节头表
hello.o的节头表 描述了.o文件中出现的各个节的类型、位置、所占空间大小等信息。

图-4-4 可重定位格式目标文件节头表 


4.3.4. hello.o的重定位节
当汇编器天生一个目标模块时,它不知道数据和代码将最终放置在内存中的位置,也不了解模块引用的任何外部定义的函数或全局变量的位置。因此,每当汇编器遇到对最终位置未知的目标引用时,它会天生一个重定位条目,指示链接器在归并目标文件成可实行文件时怎样修改这个引用。
图4-5描述了重定位节.rela.text和.rela.eh_frame的内容。重定位节.rela.text中存储了代码的重定位条目。在.rela.text节中,列出了.text节中位置的列表,必要在链接器归并这个目标文件和其他文件时修改这些位置。根据重定位条目标不同类型,链接器会实行不同的操纵。例如,对于类型R_X86_64_PC32,它表示重定位一个利用32位PC相对地点的引用。因此,链接器在修改这种类型的重定位条目对应的引用时,会按照PC相对寻址的方式举行修改。同时,在.rela.text中的偏移量表示这些符号在.text节中的偏移位置。通过利用objdump -d -r hello.o命令,我们可以检察反汇编后.text节的信息,可以发现这与.rela.text中的重定位条目标信息符合。
关于重定位节.rela.eh_frame的内容,受笔者水平所限,无法举行更深入的分析。
hello.o需重定位的内容:.rodata中的模式串,puts,exit,printf,atoi,sleep,getchar等符号。

图-4-5 可重定位格式目标文件重定位节


4.3.5. hello.o的符号表
.symtab存放在程序中定义和引用的函数和全局变量的信息。如图4-6所示,包括Value、Size、Type、Bind、Vis、Ndx、Name等信息。

       图-4-6 可重定位格式目标文件符号表

4.4 Hello.o的结果分析

反汇编命令:objdump -d -r hello.o > dump_hello.txt
与hello.s相比,hello.o的反汇编中大部门指令保持不变。然而,唯一的区别在于,类似语义的指令如mov、add、push等,在hello.s中会带有相应的后缀q、l等,而在hello.o的反汇编中则没有后缀。别的,汇编语言与机器语言的构造情势不同。在hello.s中,开头包含了对常量和变量的描述,而在hello.o的反汇编代码中,则没有这些描述。机器语言与汇编语言之间存在一一映射的关系。例如,如图4-7所示,hello.o的反汇编代码在每条指令前还包含了相对应的机器码的十六进制表示。

图-4-7 hello.o反汇编代码与hello.s对比

其次如图4-7圆圈所示,hello.s中的操纵数用十进制表示,而 hello.o的反汇编代码中的操纵数用十六进制表示。
如图4-8所示,hello.s在跳转时,控制转移指令(je)后接的是段名(.L2),而在hello.o的反汇编代码中则利用了十六进制相对地点;如图4-8所示,hello.s在函数调用时,函数调用指令(call)跟的是函数名(puts),而在hello.o的反汇编代码中则同样利用了十六进制相对地点。

图-4-8 hello.o反汇编代码与hello.s控制转移与函数调用对比


机器语言是由二进制代码表示的,计算机可以大概直接识别和实行的一种机器指令的聚集。每条机器语言指令由操纵码和操纵数构成。操纵码规定了指令的操纵,是指令中的关键字,不可省略。而操纵数则表示该指令的操纵对象[4]。
机器语言与汇编语言之间存在着一种映射关系:对于汇编语言中的每个指令,如movq、leaq、popq等,在机器语言中都有相应的操纵码对应。别的,不同的操纵寄存器也会导致操纵码的不同。例如,对于类似的mov指令,如果将立即数放入eax寄存器和放入esi寄存器,它们的操纵码将不同。总的来说,对于汇编语言中的一条指令,机器语言会根据其操纵、操纵对象和目标对象的不同举行调整和厘革。
机器语言中的操纵数与汇编语言并不完全划一。如果汇编语言中是绝对地点或立即数,那么在机器语言中,操纵数将以该绝对地点的小端表示情势出现(大端小端表示与操纵体系有关)。而如果汇编语言中利用了PC相对寻址,那么在机器语言中,操纵数将表示为下一条指令地点与目标地点之间的差值(即相对值)。
4.5 本章小结

本章具体阐述了“编译”过程,起首先容了汇编的定义和作用,接着演示了如安在Linux环境下将hello.s汇编文件转换为hello.o目标文件,并分析了hello.o的elf格式以及比较了hello.s与hello.o的反汇编代码之间的联系和差别。
此时,最初的hello.c源代码文本文件已经经过处理转换成了hello.o二进制文件,经过了汇编的C语言程序,如今有着十分清晰的布局,即ELF格式。只管此时的二进制文件已经可以大概被CPU所识别,但它尚未与内存或库函数的代码和数据相结合,因此无法作为最终的可实行目标文件。在接下来的章节中,即“链接”之后,hello将会最终形成,并变化为可实行的目标文件。

5链接


5.1 链接的概念与作用

5.1.1.链接的概念
链接是将各种代码和数据片断收集并组合成为一个单一文件的过程,这个文件可被加载到内存并实行[3]。这一过程由链接器完成。链接器(ld)将各种不同文件(主要是可重定位目标文件)的代码和数据综合在一起,通过符号分析和重定位等操纵,最终组合成一个可以在程序中加载和运行的单一的可实行目标文件。
5.1.2.链接的作用
链接使得分离编译成为可能,即不必要将一个大型的应用程序构造成一个巨大的源文件,而是可以将其分解成更小的、更好管理的模块,可以独立的修改和编译这些文件模块,同时使我们的程序可以大概乐成访问到它引用的所有目标模块,从而保证了我们的可实行程序可以在机器上顺利实行。
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下利用ld的链接过程

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

分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地点,大小等信息。可实行目标文件的格式与可重定位目标文件的格式相似。它们都包含ELF头来描述文件的总体格式,而且包括程序的入口点,即程序运行时要实行的第一条指令的地点。.text、.rodata以及.data节与可重定位目标文件的节是相似的。然而,可实行目标文件的elf格式多了一个.init节,而且定义了一个小函数叫做_init,程序的初始化代码会调用它。由于可实行文件已经完全链接(已被重定位),以是不再必要rel节,即重定位节。此处利用readelf指令检察hello的ELF格式,包括各段起始地点,大小等信息。

图-5-2 可实行文件的ELF布局图


5.3.1. hello的ELF头
利用readelf -h hello命令检察hello的ELF头:

图-5-3 可实行文件hello的ELF头


5.3.2. hello的节头表
利用readelf -S hello命令检察hello的节头表:


图-5-4 可实行文件hello的部门节头表


5.3.3. hello的程序头表
调用readelf -l hello检察hello的程序头表:

图-5-5 可实行文件hello的程序头表1


图-5-6 可实行文件hello的程序头表2


此中Offset代表目标文件中的偏移,VirtAdrr代表虚拟内存的地点,PhysAddr代表物理地点的内存,FileSiz代表目标文件中的段大小,MemSiz代表内存中的段大小,flags代表运行时的访问权限,align代表对齐要求。
5.4 hello的虚拟地点空间

利用edb加载hello, data dump窗口可以检察加载到虚拟地点中的hello程序。program headers告诉链接器运行时加载的内容并提供动态链接必要的信息。  
由下图可以得到,虚拟地点空间的起始位置是0x400000:

图-5-7 hello的虚拟地点空间起始位置


由前文可知,.interp段的起始地点为0x400000+0x0002e0=0x4002e0,在edb中查找地点得到如下图的结果:

图-5-8 hello的.interp节存储内容


由前文中图21可以得到,.rodata的起始地点为0x402000,在edb中查询地点可以得到如下图的结果:

图-5-9 hello的.rodata节存储内容


   根据《深入明白计算机体系》一书中先容了Linux x86_64运行时内存映像(如下图所示,书中图7-15),我们可以知道只读代码段中包括了.init节、.text节和.rodata节。刚才探求到了.rodata节在内存中的信息,下面继续通过edb举行探求。

图-5-10 Linux X86-64运行时内存映像


在节头部表中没有找到.init节的相关信息,笔者尚不清晰缘故起因。
通过节头部表,可以知道.text节位于虚拟内存0x4010d0处。通过运行edb,0x4010d0处正是main函数的起始位置。

图-5-11.text节在虚拟内存中

由于hello.c中没有定义全局变量和静态变量,因此不再去探求读/写段在内存中的映射。不过根据节头部表,.data节在内存中有4个字节的空间,笔者在此之前并不清晰。
5.5 链接的重定位过程分析

反汇编命令:objdump -d -r hello > hello_objdump.s

图-5-12 hello的反汇编代码中.init函数


5.5.1.新增函数
链接参加了在hello.c中用到的库函数,如printf、getchar、exit、sleep等函数。


图-5-13 hello的反汇编代码中的新增函数


5.5.2.新增节
hello中增加了如图5-12与图5-14所示的.init和.plt节: 

图-5-14 hello的反汇编代码中的新.plt节


5.5.3.函数调用地点
Hello已经完成了调用函数时的重定位,因此在调用函数时调用的地点已经变成了函数确切的虚拟地点:

图-5-15 hello的反汇编代码中的函数调用利用的地点


5.5.4.控制流跳转地点
Hello已经完成了调用函数时的重定位,因此在跳转时调用的地点已经变成了函数确切的虚拟地点。

图-5-16 hello的反汇编代码与hello.o反汇编代码的控制流跳转对比

5.6 hello的实行流程

如今对hello.o举行重新编译,参加-g便于后续利用edb调试:
命令:gcc -m64 -Og -no-pie -fno-stack-protector -fno-PIC hello.c -o hello1

图-5-17 hello1的函数列表
程序的调用次序是:

  • 调用了动态链接库linux-x86-64.so.2、libc.so中的几个函数
  • _start(0x401160)
  • __libc_start_main(0x7f2be0029dc0)
  • __cxa_atexit(0x7f2be00458c0)
  • libc.so中几个函数(0x7f2be00456d0)
  • 动态链接库libc.so.6里的函数(包括_setjmp等)
  • hello中main函数(0x4010d0)
  • __printf_chk@plt(0x401090)
  • strtol@plt(0x401080)
  • sleep@plt(0x4010b0)
  • 重复8/9/10步骤9次
  • getc@plt(0x4010c0)
  • libc.so.6!exit
5.7 Hello的动态链接分析

当程序调用一个由共享库定义的函数时,由于该函数所在的共享模块可能会加载到内存的任意位置,编译体系采用延迟绑定的计谋,将过程地点的绑定推迟到第一次调用该过程的时间。动态链接器利用过程链接表(PLT)和全局偏移量表(GOT)来实现函数的动态链接。在GOT中存放函数的目标地点,而PLT则利用GOT中的地点跳转到目标函数。PLT是一个数组,此中每个条目是16字节的代码。PLT [0]是一个特殊的条目,它跳转到动态链接器中。而GOT是另一个数组,此中每个条目是8字节的地点。GOT [2]是动态链接器在1d-linux.so模块中的入口点。每个条目都对应一个匹配的PLT条目。
在第5.3部门的图5-4中,展示了hello的ELF文件。此中显示了GOT和PLT在运行时的地点分别为0x403ff0和0x404000。因此,在调用dl_init之前和之后,对比GOT在运行时的地点如图5-18和图5-19所示。

图-5-18 调用dl_init前PLT的内容

图-5-19 调用dl_init后PLT的内容
5.8 本章小结

本章主要探究了链接的过程。起首先容了链接的定义和作用,随后演示了将hello.o文件链接为可实行文件hello,并通过edb工具检察hello程序的虚拟地点空间。别的,对比了hello与hello.o的反汇编代码,具体表明了链接过程中的重定位、实行流程以及动态毗连等关键步骤。
至此,hello程序已经准备停当。它已经走过了链接的一半路程,接下来将会被加载到CPU中,并由shell构造成一个在操纵体系中运行的进程。

6hello进程管理


6.1 进程的概念与作用

6.1.1.进程的概念
进程(Process)的经典定义是一个实行中的程序的实例;是计算机中的程序关于某数据聚集上的一次运行运动,同时也是体系举行资源分配和调理的基本单位,是操纵体系布局的基础。
体系中的每个程序都运行在某个进程的上下文中。程序的上下文由程序精确运行所必要的状态组成:包括存放在内存中的程序的代码和数据、栈、通用寄存器的内容、程序计数器、环境变量以及打开文件描述符的聚集。
6.1.2.进程的作用
进程给程序提供了一个独立的逻辑控制流,提程序独占利用处理器的假象,提供了程序独占利用内存体系的假象;进程给程序提供了一个私有的地点空间。进程概念的提出,可以使得多个程序在我们的计算机中并发地运行。
6.2 简述壳Shell-bash的作用与处理流程

Shell是一个交互型的应用程序,它代表用户实行其他程序。通过实行一系列的读取/求值步骤,Shell读取用户的命令行输入,分析命令,然后代表用户运行程序。Shell的主要功能包括进程的创建、程序的加载与运行,前后台控制,作业调理,以及信号的发送与管理等。
Shell的处理流程大抵如下:
(1)用户输入命令行指令(例如./hello);
(2)Shell根据用户在命令行输入的字符串举行处理息争释。随后,根据不同的命令采取不同的操纵;
(3)如果必要运行其他程序,则Shell通过fork创建一个子进程(对于内置命令,Shell会直接实行,而不会创建新的进程);
(4)在子进程中,通过execve函数加载并运行目标程序,并为新程序的代码段、数据段、bss段以及栈段举行新的映射。
6.3 Hello的fork进程创建过程

父进程通过调用fork函数创建一个新的运行的子进程。fork函数是一个体系调用,在用户程序和内核之间提供了一个接口。当shell在调用fork函数的时间,会通过syscall这条陷阱指令,举行fork的体系调用。之后会调用相应的体系调用服务例程sys_fork()实行,sys_fork()最终会创建一个子进程,其内存映射与父进程完全类似(子进程完全复制了父进程的mm_struct、vm_area_struct数据布局和页表,同时它将父进程和子进程中每个私有页面都标记为只读,并将两个进程中每个区域布局都标记为私有的写时复制)。这样就完成了子进程的创建,之后父进程会返回陷阱指令syscall的下一条指令,继续实行下去。子进程中,fork返回0;父进程中,返回子进程的PID;此时子进程与父进程具有以下性子:
(1)子进程得到与父进程虚拟地点空间类似的(但是独立的)一份副本;
(2)子进程还获得与父进程任何打开文件描述符类似的副本,即:子进程可以大概读写父进程所打开的任何文件;
(3)子进程获得与父进程任何打开文件描述符类似的副本;
(4)最大区别:子进程有不同于父进程的PID。
(5)父进程和子进程是并发运行的独立进程。内核以任意方式瓜代实行其逻辑控制流的指令,因此无法猜测实际父进程与子进程的运行先后;
6.4 Hello的execve过程

在shell创建的子进程中,利用execve函数加载并运行我们的hello程序。当子进程调用execve函数时,会触发syscall陷阱指令,实行execve体系调用。在陷阱处理程序中,会调用相应的体系调用服务例程sys_execve()来实行。sys_execve()起首会接纳或重新初始化当前进程的资源,如删除当前进程虚拟地点的用户部门中已存在的区域布局,重新初始化进程控制块等资源。随后,它会调用操纵体系的加载器,将hello程序加载到当前进程的虚拟空间中,包括映射新程序的代码段、数据段等私有区域,映射共享区域,并设置程序计数器。值得留意的是,execve乐成调用时不会返回到调用程序,只有在出现错误时才会返回到调用程序。
6.5 Hello的进程实行

6.5.1.逻辑控制流
进程的运行本质上是CPU不断从程序计数器 PC 指示的地点处取出指令并实行,此中这些PC值的序列叫做逻辑控制流,而且这些值与可实行目标文件的指令大概包含在运行时动态链接到程序的共享对象中的指令一一对应。
6.5.2.进程时间片
操纵体系对进程的运行举行调理,采取实行进程A、上下文切换、实行进程B、再次上下文切换、实行进程A的循环。在进程实行的某些时刻,内核可能会决定抢占当前进程,并重新开始先前被抢占的进程,这个决策过程被称为调理,由内核中的调理器代码来处理。当内核选择一个新的进程运行时,我们称内核调理了该进程。一旦内核调理了新的进程运行,它就会抢占当前进程,并利用上下文切换机制将控制权转移到新的进程。在一个程序从被调用开始到被另一个进程打断之间的时间段被称为运行的时间片。
当一个逻辑流的实行与另一个流的实行在时间上重叠时,称为并发流,这两个流同时运行。多个流同时实行的概念称为并发。当一个进程与其他进程瓜代运行时,这种概念称为多任务。一个进程在其控制流的每个时间段内的实行称为一个时间片,多任务也被称为时间分片。
6.5.3.用户与内核模式
用户模式的进程不允许实行特殊指令,不允许直接引用地点空间中内核区的代码和数据。
内核模式进程可以实行指令集中的任何命令,而且可以访问体系中的任何内存位置。
运行程序代码初始时都是在用户模式中的,当发生停止故障或体系调用的异常时,进程从用户模式变化为内核模式。当异常发生时,控制传递到异常处理程序,处理器将模式变化为内核模式。
6.5.4.进程的上下文切换
在内核调理了一个新的进程运行后,它就抢占当前进程,并利用一种称为上下文切换的机制来将控制转移到新的进程,上下文切换——①保存当前进程的上下文,②恢复某个先前被抢占的进程被保存的上下文,③将控制传递给这个新恢复的进程。
上下文切换一定发生在内核模式下。因此关于时间分片的实现大要上是这样的:由于每个体系都有某种产生周期性定时器停止的机制(一般为1ms或10ms),当hello进程在它的时间片(具体来说应该就是定时器的单个周期)里实行的时间,如果发生了定时器的停止,hello进程由于停止异常会变为内核模式,此时内核可以大概判定hello进程已经运行了足够长的时间,并会通过上下文切换切换到新的进程。
除了定时器停止,在内核代表用户举行体系调用的时间,也可能会发生上下文切换,切换到新的进程。总体而言,进程上下文切换过程如图6-1:

图-6-1 进程上下文切换示意图

6.6 hello的异常与信号处理

6.6.1.正常实行
在正常运行过程中,主要会发生陷阱异常和故障异常。当程序开始实行_start时,此时内存中并没有hello程序的代码和数据。当CPU读取hello中的第一条指令时,会触发缺页故障。这时,体系会进入内核模式,并实行相应的缺页异常处理程序。缺页处理程序会根据所得到的物理地点,将相应的物理页缓存到内存中,并更新相应的页表条目内容。
别的,在正常运行过程中,还会因举行体系调用而触发陷阱异常(例如调用exit函数等)。当程序实行syscall指令时,会触发一个到异常处理程序的陷阱。该处理程序会分析参数,并调用适当的内核程序。这可能会产生SIGCHLD信号。当hello程序停止时,会向shell发送SIGCHLD信号,shell在接收到信号后会接纳子进程。

图-6-2 正常运行状态

6.6.2.乱按键盘
在乱按键盘的时间会发生停止异常,hello进程会进入内核模式,将控制转移给停止异常处理程序。键盘的停止处理程序,会从键盘控制器的寄存器读取扫描码并翻译成ASCII码,并存入键盘缓冲区。通过几次试验,在按了回车键的时间,输入的字符串会被shell识别为命令。

图-6-3 乱按键盘后运行状态

6.6.3.Ctrl+C
在按下Ctrl+C时除了会发生键盘的停止异常,还会使hello进程收到一个SIGINT的信号,结果是使hello进程停止。在hello进程停止了之后又会给shell进程发送SIGCHLD信号,处理方法如前文所述。

图-6-4 Ctrl+C

6.6.4.Ctrl+Z
在按下了Ctrl+Z时,hello进程会收到一个SIGTSTP的信号,结果是使hello进程停止。此时hello进程不再是前台,从而shell没有前台作业必要等待。我们可以在shell中继续输入命令。用ps命令可以检察当前的所有进程的进程号,用jobs命令检察所有的作业,用fg命令可以将指定的作业放在前台运行,此时会给指定的进程组发送SIGCONT信号,让挂起的进程重新运行。用kill命令可以向指定的进程组发送信号,kill -9表示发送SIGINT信号,会让进程组内每一个进程停止。

图-6-5 Ctrl+Z

6.7本章小结

本章具体探究了hello进程的实行过程。起首,先容了fork、execve以及实行的流程,并对hello运行期间涉及的进程、shell-bash、fork、execve、进程实行以及异常与信号处理等操纵举行了深入阐述。接下来,从逻辑控制流、时间片、用户模式/内核模式以及上下文切换等方面临进程的实行过程举行了具体分析。最后,对hello的异常与信号处理机制举行了分析,并演示了在不同环境下hello发生异常的具体环境。

7hello的存储管理


7.1 hello的存储器地点空间

7.1.1.逻辑地点
逻辑地点指的是:程序经过编译后出如今汇编代码中的地点,是由程序产生的与段相关的偏移地点部门,也叫相对地点;一个逻辑地点由一个段和偏移量组成。表示为[CS:EA]。在实模式下:物理地点=CS*16+EA;在保护模式下:以段描述符作为下标,到GDT/LDT表查表获得段地点,段地点+偏移地点=线性地点。
hello.o中所利用的即为逻辑地点,要经过寻址方式的计算或变动才得到内存储器中的实际有效地点,即物理地点。
7.1.2. 线性地点
线性地点是逻辑地点到物理地点变动之间的中间层。代码会产生段中的偏移地点,加上相应段的基地点就天生了一个线性地点。线性地点实际上是非负整数地点的有序聚集。在保护模式下,线性地点=段地点+偏移地点=线性地点。
7.1.3.虚拟地点
CPU启动保护模式之后,程序运行在虚拟地点空间中,虚拟地点空间是所有可能地点的聚集,对于一个64位的机器而言,则聚集中共有2^64种可能。
但并非所有程序均运行在虚拟地点当中,CPU在启动的时间是运行在实模式的,是直接利用物理地点的。
7.1.4. 物理地点
物物理地点是指存储器中以字节为单位存储信息时,每个字节单位与一个唯一的存储器地点一一对应的地点。这些地点也被称为实际地点或绝对地点,它们通过寻址总线传送。物理地点对应体系中实际的内存字节。
7.2 Intel逻辑地点到线性地点的变动-段式管理

在Intel平台下,逻辑地点以selectorffset的情势表示,此中selector是CS寄存器的值,而offset是EIP寄存器的值。通过将selector与全局描述符表(GDT)中相应的段基址相加,得到线性地点。这个过程被称为段式内存管理。
逻辑地点由段标识符和段内偏移量组成。段标识符是一个16位长的字段,也就是段选择符。通过段选择符的前13位,可以直接在段描述符表中找到对应的段描述符。全局段描述符存放在全局段描述符表(GDT)中,而一些局部的段描述符则存放在局部段描述符表(LDT)中。
给定完备的逻辑地点(段选择符+段内偏移地点),通过检查段选择符的T1位(位13),可以确定要转换的是GDT中的段还是LDT中的段。然后根据相应的寄存器,可以得到段的地点和大小。通过在段描述符表中查找对应的段描述符,可以获取其基地点。最终,将基地点与偏移量相加得到线性地点。

图-7-1 逻辑地点向线性地点转换
7.3 Hello的线性地点到物理地点的变动-页式管理

假设虚拟页和物理页的大小均为4k(12位),通过存储在物理内存中的一个数据布局页表,可以实现将线性地点转化成物理地点。页表是页表条目标数组,每一个页表条目(PTE),都由一个有效位和一个物理页号(PPN)组成。对于一个n位的虚拟地点,其低12位是页内偏移(VPO),其余位表示着其虚拟页号(VPN)。
CPU内里有一个控制寄存器PTBR(页表基址寄存器),指向当前页表。通过PTBR找到页表的首地点,再根据VPN的值可以得到对应的页表条目标地点(PTEA)。PTEA=%PTBR+VPN*页表条目大小。找到了页表条目后,如果有效位=1,说明该虚拟页缓存进了内存,从而根据PTE可以找到该虚拟页对应的物理页号。由于虚拟页和物理页大小相等,物理页中的页内偏移PPO=VPO。从而物理地点由PPN与VPO组合而成。(具体过程可见《深入明白计算机体系》图9-12,见下图)。如果有效位=0,则会发生缺页故障。
    

图-7-2 利用页表举行地点翻译
7.4 TLB与四级页表支持下的VA到PA的变动

TLB是一个位于MMU中,关于PTE的一个缓存,被称为快表。快表是一个小的、虚拟寻址的缓存,此中每一行均保存了一个由单个PTE组成的块。TLB有高度的相联性。
四级页表是一种多级页表,多级页表的主要目标是用于压缩页表。在地点翻译过程中,虚拟的地点页号VPN被分为了k个,每一个VPNi都是一个指向第i级页表的索引。当1 <= j <= k-1时,都是指向第j+1级的某个页表。第k级页表中的每个PTE包含某个物理页面的PPN,大概时一个磁盘块的地点。

图-7-3 PTE组成布局


TLB利用虚拟地点的VPN部门举行索引,分为索引(TLBI)和标记(TLBT)两个部门。这使得MMU在读取页表项(PTE)时可以直接从TLB获取,如果未掷中则必要将PTE从内存复制到TLB中。
基于以上机制,纵然只利用虚拟地点空间的一小部门,仍必要与利用更多空间类似的页表,导致内存浪费。因此,虚拟地点到物理地点的转换过程中引入了多级页表机制:上一级页表映射到下一级页表,直至页表映射到虚拟内存。如果下一级内容未分配,页表项将为空,不映射到下一级,也不会存在下一级页表。只有在必要时才会创建相应的页表,以节约内存空间。

图-7-4 VA到PA的变动

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

对于一个物理地点PA,可以分为三部门:标记(CT)、组索引(CI)、块偏移(CO)。起首,根据组索引在L1缓存中选择相应的组。检查有效位是否为1,如果是,则将标记与该组中每一行的标记举行比较。如果匹配乐成,则根据块偏移在掷中的块中读取数据;如果未掷中,则访问L2缓存。访问方式与之前类似:先检查组索引,然后有效位,最后匹配标记。如果掷中,则读取数据;如果未掷中,则继续访问下一级存储器。如果在三级缓存中都未掷中,则必要从内存中读取数据,并将该块参加到L3缓存中。
7.6 hello进程fork时的内存映射

Shell通过fork为必要实行的非内置命令创建新的进程。当当前进程调用fork函数时,内核会为新进程创建各种数据布局,并分配给它一个独特的PID。为了为新进程创建虚拟内存,内核会复制当前进程的mm_struct、区域布局和页表的副本。它会将两个进程中的每个页面标记为只读,并将每个进程中的每个区域布局标记为写时复制。
当fork在新进程中返回时,新进程的虚拟内存与调用fork时当前进程的虚拟内存类似。当这两个进程中的任意一个举行写操纵时,写时复制机制会创建新的页面。
7.7 hello进程execve时的内存映射

当hello进程通过execve加载并运行hello程序时,execve函数有效地替换了当前程序。execve函数对内存映射实行以下操纵:1.清除当前进程虚拟地点空间中已存在的用户部门区域布局。2.为hello程序的代码、数据、bss和栈区域创建新的区域布局。代码和数据区域被映射到hello可实行文件中的.text和.data节,bss区域初始化为二进制零,并映射到匿名文件,栈和堆区域也被初始化为二进制零,初始长度为0,并映射到匿名文件。3.映射共享区域。由于hello程序与共享对象(如libc.so共享库)链接,因此这些对象被动态链接到该程序,并映射到用户虚拟地点空间的共享区域内。
7.8 缺页故障与缺页停止处理


图-7-5 缺页异常处理


缺页故障:一个虚拟页没被缓存在DRAM中,即DRAM缓存不掷中被称为缺页。若程序想要访问某个虚拟页中的数据的时间,会产生一个虚拟地点。当CPU引用了一个页表条目中的一个字,而该页表条目并未被缓存在DRAM中,MMU(内存管理单位)在试图翻译这个虚拟地点的时间,会发现该地点所在的虚拟页没有缓存进内存(即PTE中有效位为0),必须从磁盘中取出,这时间就会触发缺页异常。
缺页停止处理:

  • 处理器天生一个虚拟地点,并将它传送给MMU;
  • MMU天生PTE地点,并从高速缓存/主存哀求得到它;
  • 高速缓存/主存向MMU返回PTE;
  • PTE中的有效位是0,以是MMU出发了一次异常,传递CPU中的控制到操纵体系内核中的缺页异常处理程序;
  • 缺页处理程序确认出物理内存中的断送页,若这个页已经被修改了,则把它换到磁盘;
  • 缺页处理程序页面调入新的页面,并更新内存中的PTE;
缺页处理程序返回到原来的进程,再次实行导致缺页的命令。CPU将引起缺页的虚拟地点重新发送给MMU。由于虚拟页面已经换存在物理内存中,以是就会掷中。
7.9动态存储分配管理

动态内存分配器维护着一个进程的堆,即虚拟内存区域。该分配器将堆视为一组不同大小的块的聚集来管理。每个块都是一段一连的虚拟内存片断,可以是已分配的或空闲的。已分配的块明确保留供应用程序利用,而空闲块可用于分配,如果未被显式分配,则会保持空闲状态。
有两种主要风格的分配器,取决于怎样释放已分配的块:

  • 显式分配器要求应用程序显式释放任何已分配的块,例如C语言中的malloc和free;
  • 隐式分配器要求分配器检查已分配的块何时不再被程序利用,然后释放该块。这种分配器也称为垃圾收集器,在Java语言中就依赖于垃圾收集来释放已分配的块;
对于实现显式分配器时,有几种方法和计谋来构造、放置、分割和归并空闲块:

  • 利用隐式空闲链表来构造空闲块(单向链表)。
放置计谋:包括初次适配、下一次适配和最佳适配等;
分割计谋:可以选择不分割,利用整个空闲块,或将其分成两部门,一部 分作为分配块,另一部门作为空闲块;
获取额外的堆内存以获得适当的空闲块;
归并空闲块的计谋:包括立即归并(每次释放一个块时立即归并)、推迟 归并和带边界标记的归并;

  • 利用显式空闲链表来构造空闲块(可以构造为双向链表)。
维护链表的次序:可以按照后进先出的次序或按地点次序。

  • 利用分离的空闲链表来维护多个空闲链表。
有两种基本方法:简单分离存储和分离适配。
7.10本章小结

本章围绕"hello"程序展开重点讨论了存储管理。内容包括储存器地点空间、段式管理、页式管理、虚拟地点到物理地点的转换、物理内存访问、"hello"进程在fork和execve时的内存映射、以及处理缺页故障和缺页停止、动态存储分配管理等。
通过这些内容,我们已经全面了解了"hello"程序。在下一章中,我们将探究我们最初编写"hello"程序时的假想,也是其最焦点的任务和使命之一:I/O交互。

8hello的IO管理


8.1 Linux的IO设备管理方法

设备的模型化:文件
设备管理:unix io接口
在Linux体系中,所有的I/O设备都被抽象为文件的情势,所有的输入和输出操纵都被当作对相应文件的读取和写入来实行。这种将设备优雅地映射为文件的方法,使得Linux内核可以大概提供一个简单而底层的应用接口,被称为Unix I/O。
在Linux中,一个文件就是一个字节序列,这种将设备映射为文件的方式,让Linux内核可以大概提供一个简单而底层的应用接口,使得所有的输入和输出操纵都可以以一种同一且划一的方式举行:(1)打开文件 (2)读写文件 (3)改变当前的文件位置 (4)关闭文件。
8.2 简述Unix IO接口及其函数

8.2.1.Unix I/O接口
(1) 打开文件:应用程序通过向内核哀求打开相应的文件来声明其对一个I/O设备的访问意图。内核会返回一个小的非负整数,称为文件描述符,在后续对该文件的所有操纵中用于标识它。内核会记录关于这个打开文件的所有信息,而应用程序只需记着这个描述符。
(2) 读写文件:读操纵是将n个字节从文件的当前位置k复制到内存中,然后将当前位置k增加n。对于一个大小为m字节的文件,在当前位置k大于等于m时实行读操纵会触发一个称为EOF的条件,应用程序可以检测到这个条件。类似地,写操纵是将n个字节从内存复制到文件中,从当前位置k开始,然后更新当前位置k。
(3) 改变当前文件位置:对于每个打开的文件,内核会维护一个文件位置k,初始位置为0,表示文件开头的字节偏移量。应用程序可以通过seek操纵显式地将文件的当前位置设置为k。
(4) 关闭文件:当应用程序完成对文件的访问后,它会通知内核关闭这个文件。作为相应,内核会释放文件打开时创建的数据布局,并将文件描述符恢复到可用的描述符池中。无论进程因何种缘故起因停止,内核都会关闭所有打开的文件并释放它们的内存资源。
8.2.2.Unix I/O函数
(1)int open(char *filename, int flags, mode_t mode);
进程通过调用open函数打开一个已存在的文件大概创建一个新文件:Open函数将filename转换为一个文件描述符,而且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags参数指明白进程筹划怎样访问这个文件:O_RDONLY:只读、O_WRONLY:只写和O_RDWR可读可写。mode参数指定了新文件的访问权限位。
(2)int close(fd):
进程调用close函数关闭一个打开的文件,fd是必要关闭的文件的描述符。
(3)ssize_t read(int fd, void *buf, size_t n);
read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
(4)ssize_t write(int fd, const void *buf, size_t n);
write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。
(5)off_t lseek(int fd, off_t offset, int whence);
Lseek函数用于修改文件偏移量,此中fd为文件描述符,offset为必要偏移的字节数,whence为开始的位置:宏定义SEEK_END,SEEK_CUR,SEEK_SET分别为文件末尾、当前位置、文件开头。
8.3 printf的实现分析

printf函数的函数体如图8-1所示

图-8-1 printf函数的函数体


va_list是一个用于参数列表的字符指针的重新定义,即typedef char* va_list。在这个上下文中,(char*)((&fmt) + 4 )代表了第一个参数,这与栈的布局有关。*fmt存放在栈中,而后续的字符型指针也都存在于栈中。由于在32位的Linux体系中,一个指针的大小为4字节,而在利用m64编译命令时,一个指针的大小为8字节。因此,通过+4来获取第一个参数的位置。
之后调用了vsprintf函数,其函数体如下图所示:

图-8-2 vsprintf函数的函数体

vsprintf函数将格式化后的参数内容存储到buf中,并返回格式化数组的长度。然后,write函数将buf中的i个元素写入终端。整个过程涉及从vsprintf天生显示信息,到write体系函数,再到陷阱-体系调用int 0x80或syscall。
字符显示驱动子程序负责将字符从ASCII码转换为相应的字模库,然后将其显示在屏幕的VRAM中,此中存储了每个像素点的RGB颜色信息。显示芯片按照设定的革新频率逐行读取VRAM,并通过信号线向液晶显示器传输每个像素点的RGB分量信息。
8.4 getchar的实现分析

getchar函数的函数体如图8-3所示

图-8-3 getchar函数的函数体


当程序调用getchar时,它开始等待用户按键。用户所输入的字符被存放在键盘缓冲区中,直到用户按下回车键(回车键也被包含在缓冲区内)。然后,getchar通过体系调用read来读取存储在键盘缓冲区中的ASCII码。getchar函数的返回值是用户输入的第一个字符的ASCII码,如果堕落则返回-1,而且将用户输入的字符回显到屏幕上。如果用户在按下回车键之前输入了多个字符,那么其他字符将保留在键盘缓冲区中,等待后续的getchar函数调用读取。换句话说,后续的getchar调用不会等待用户按键,而是直接读取缓冲区中的字符,直到缓冲区中的字符被读取完毕,才会再次等待用户按键。
在我们乱按键盘时,会触发硬件停止异常,导致hello进程进入内核模式,并将控制权转移到键盘的停止处理程序。该处理程序接收按键扫描码并将其转换成ASCII码,然后保存到体系的键盘缓冲区中。getchar函数调用read体系函数,在实行过程中会通过syscall指令举行体系调用,触发陷阱异常,导致进程进入内核模式并实行陷阱处理程序。该处理程序分析参数并调用内核程序,以读取ASCII码值,直到接收到回车键信号后才返回。
对于异步异常,即键盘停止的处理,键盘停止处理程序会接收按键扫描码并将其转换成ASCII码,然后保存到体系的键盘缓冲区中。接着,getchar等函数调用read体系函数,通过体系调用读取按键的ASCII码,直到接收到回车键信号后才返回。
8.5本章小结

本章主要先容了linux体系对于I/O设备的管理方法,并简单叙述了Unix I/O接口以及相应的函数,而且对于此中的printf与getchar函数的具体实现举行了分析。
至此hello程序的演出正式谢幕,他的生命走到了尽头。
结论

结论:
至此hello已经走完了他的一生的全部旅程,同时作为每个程序员的“初恋程序”我们借助hello对于计算机体系以及一个程序从编写到最终运行实现的全过程也有了更加深入的了解与体会,同时再次回首hello所走过的一生: 

  • 编写hello.c的源程序,完成hello.c文件;
  • 预处理:经过预处理,通过宏替换、文件包含等步骤,调用的库以及宏定义与源文本文件替换归并得到完备庞大的hello.i文本文件;
  • 编译:通过词法分析、语法分析和语义分析等向程序员提出各种可能的报错和告诫。经过多次修改、反复编译之后,将文本文件hello.i编译天生汇编语言文件hello.s;
  • 汇编:将汇编语言文件hello.s汇编得到二进制可重定位目标文件hello.o,这时间,hello.o已经是一个机器语言文件了,可以大概被机器识别但是由于不完备无法被实行;
  • 链接:hello.o与其它调用库函数所在的可重定位目标文件和动态链接库链接天生可实行文件hello,至此hello可以被加载入内存并运行;
  • 创建进程:终端shell调用fork函数,创建一个子进程,在子进程中通过execve将hello加载到了内存,子进程为程序的加载运行提供虚拟内存空间等上下文;
  • 加载程序:终端shell调用execve函数,启动加载器映射虚拟内存,之后开始载入物理内存,在进入main函数;
  • 访问内存:通过TLB和多级页表,实现虚拟内存和物理内存的翻译,进而访问计算机的存储布局,访问内存;
  • IO:hello输入输出与外界举行交互; 
  • 停止:hello向shell发送SIFCHLD信号,被父进程接纳,内核收回为其创建的所有信息。

感悟:
计算机体系的运行远比我们所见到和想象到的复杂得多。它凝聚了无数计算机范畴先驱的智慧和努力。这些体系通过精巧的设计,美满地整合了软件和硬件,使得建立在其之上的所有应用可以大概更加稳定高效地运行。回首我对计算机科学的学习进程,我深刻领悟到了纵然是看似简单的操纵和设计背后,也可能蕴含着巨大而精妙的构思。因此,我在将来的学习中将保持谦逊和专注,不断努力研讨,寻求至善的境界。

附件



  • 1. hello.c:源程序文件
  • 2. hello.i:hello.c预处理后的源程序文件
  • 3. hello.s:hello.i编译后的汇编程序
  • 4. hello.o:hello.s汇编后的可重定位目标文件
  • 5. elf.txt:hello.o的ELF文件
  • 6. dump_hello.txt:hello.o的反汇编代码
  • 7. hello:hello.o链接后的可实行目标文件(不包含可调试选项)
  • 8. hello1:hello.o链接后的可实行目标文件(包含可调试选项)
  • 9. hello_objdump.s:hello的反汇编代码

参考文献



  •  王秀芳,孙承爱 &路燕. (2010).  C语言中编译预处理命令的解读与应用.  电脑编程本领与维护  (22),  22-24.  doi:10.16184/j.cnki.comprg.2010.22.019.
  •  袁春风. 计算机体系基础. 北京:机械工业出版社,2014
  •  Aho,Lam,Sethi,et al. Compilers : principles, techniques and tools.赵建华,郑涛,戴新宇,译.北京:机械工业出版社,2009
  •  机器语言_百度百科(baidu.com)
  •  ELF 文件分析 1-前述+文件头分析 - 知乎 (zhihu.com)
  •  ld(1) command_ld命令-CSDN博客
  •  Elf第二讲,ELF程序头 - Android_IBinary - 博客园 (cnblogs.com)
  •  shell 命令实行次序 一_shell脚本的运行次序-CSDN博客
  •  用户态--fork函数创建进程_fork创建进程-CSDN博客
  • Markmap (oscc.cc)
  • Linux文件编程常用函数详解——lseek()函数_linux lseek-CSDN博客
  • c/c++中printf函数的底层实现_printf底层实现-CSDN博客


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

诗林

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表