程序人生-Hello’s P2P

乌市泽哥  金牌会员 | 2024-9-18 06:23:10 | 来自手机 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 677|帖子 677|积分 2031

 

Hello,一个十分简单的程序,可以说是几乎全世界的程序员编写的第一个程序,我们一行一行地对着教程缓慢地输入Hello的几行代码,点击运行,惊喜地看到屏幕中输出的“Hello,World!”,然后就迅速地爱上了其他程序,却又不再回头,哪怕再多观望它一眼。然而,可以这么说,在这个简单的Hello中,囊括一个程序运行的全部过程,蕴含着无数计算机科学家的思想英华。从它的诞生再到它的逝去,它经历了每一个程序都会经历的一切:从预处理再到编译,又从编译到汇编,再从汇编到链接……在这个程序的背后,是操作系统和硬件的精密配合,使用巧妙的抽象将一个复杂巨大的过程简化为一个最基础的程序。
故本文从这个Hello程序出发,从计算机系统层面上的各个方面去游览它的生命周期,触摸程序在计算机当中的脉搏,感受它平凡却又精彩的一生。

关键词:计算机系统;Hello程序;Linux;                           









 


第1章 概述... - 5 -
1.1 Hello简介... - 5 -
1.2 环境与工具... - 5 -
1.3 中心效果... - 6 -
1.4 本章小结... - 7 -
第2章 预处理... - 8 -
2.1 预处理的概念与作用... - 8 -
2.2在Ubuntu下预处理的命令... - 8 -
2.3 Hello的预处理效果解析... - 9 -
2.4 本章小结... - 9 -
第3章 编译... - 10 -
3.1 编译的概念与作用... - 10 -
3.2 在Ubuntu下编译的命令... - 10 -
3.3 Hello的编译效果解析... - 10 -
3.4 本章小结... - 14 -
第4章 汇编... - 16 -
4.1 汇编的概念与作用... - 16 -
4.2 在Ubuntu下汇编的命令... - 16 -
4.3 可重定位目的elf格式... - 16 -
4.4 Hello.o的效果解析... - 22 -
4.5 本章小结... - 24 -
第5章 链接... - 26 -
5.1 链接的概念与作用... - 26 -
5.2 在Ubuntu下链接的命令... - 26 -
5.3 可实行目的文件hello的格式... - 26 -
5.4 hello的假造地点空间... - 31 -
5.5 链接的重定位过程分析... - 34 -
5.6 hello的实行流程... - 37 -
5.7 Hello的动态链接分析... - 39 -
5.8 本章小结... - 40 -
第6章 hello进程管理... - 42 -
6.1 进程的概念与作用... - 42 -
6.2 简述壳Shell-bash的作用与处理流程... - 42 -
6.3 Hello的fork进程创建过程... - 43 -
6.4 Hello的execve过程... - 44 -
6.5 Hello的进程实行... - 44 -
6.6 hello的异常与信号处理... - 46 -
6.7本章小结... - 51 -
第7章 hello的存储管理... - 52 -
7.1 hello的存储器地点空间... - 52 -
7.2 Intel逻辑地点到线性地点的变换-段式管理... - 52 -
7.3 Hello的线性地点到物理地点的变换-页式管理... - 54 -
7.4 TLB与四级页表支持下的VA到PA的变换... - 55 -
7.5 三级Cache支持下的物理内存访问... - 56 -
7.6 hello进程fork时的内存映射... - 57 -
7.7 hello进程execve时的内存映射... - 57 -
7.8 缺页故障与缺页中断处理... - 58 -
7.9动态存储分配管理... - 59 -
7.10本章小结... - 60 -
第8章 hello的IO管理... - 62 -
8.1 Linux的IO装备管理方法... - 62 -
8.2 简述Unix IO接口及其函数... - 62 -
8.3 printf的实现分析... - 63 -
8.4 getchar的实现分析... - 64 -
8.5本章小结... - 65 -
结论... - 65 -
附件... - 67 -
参考文献... - 68 -


(注:截图懒得转格式了 如果有必要的话可以私聊)
第1章 概述

1.1 Hello简介




      • P2P的过程


P2P,即From Program to Process,就是指Hello从一个程序变成一个进程的过程,当我们通过高级语言(C)将Hello程序的代码敲进电脑并生存为hello.c时,这就创建了一个Hello程序。接着再通过GNU编译系统自带的gcc驱动程序,它会将我们的程序通过cpp预处理器将hello.c翻译成一个ASCII码的中心文件hello.i,再通过ccl编译器,将hello.i编译成ASCII的汇编语言文件hello.s,之后再通过as汇编器,将hello.s翻译成一个可重定位目的文件hello.o,末了颠末ld链接器,将hello.o和一些系统的目的文件最终创建一个可实行目的文件hello,此时可以说,Hello,一个完善的生命就诞生了。
然而,想要到进程的过程还没那么简单,我们打开Shell-bash中输入./hello,而由于hello并不是一个Shell的内置命令,它将其识别为一个可实行目的文件,进而为这个程序创建一个子进程Process(fork),并为它execve、mmap等,使得它这个程序可以或许在这个进程中“驰骋“,而这就是Process的产生过程,亦即实现了P2P:From Program to Process。




      • 020的过程


020,即From Zero to Zero,壳shell为hello通过fork生成子进程,再在子进程中用execve调用加载器加载hello程序,通过内存映射创建假造内存空间,将数据加载到假造内存空间中去,而内核又为程序分配时间片实行逻辑控制流,直至程序结束。此时hello所在的子进程会向父进程发送SIGCHLD信号,而Shell作为父进程采取该进程,清空该程序所占的空间,删除有关的数据布局,抹去全部陈迹。这就实现了020:From Zero to Zero。
1.2 环境与工具

列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
1.2.1 硬件环境
      系统类型:X64
      CPU:12th Gen Intel(R) Core(TM) i7-12700H   2.30 GHz
      RAM:16.0 GB
      HHD:476.92 GB

图 1 CPU-Z截图
1.2.2 软件环境
Window11 64位、Vmware 16、Ubuntu 22.04 LTS 64位
1.2.3 开发与调试工具
GCC、Codeblocks、vim、gcb
1.3 中心效果

列出你为编写本论文,生成的中心效果文件的名字,文件的作用等。
1.   hello.c:源代码文件。
2.   hello.i:预处理后的文本文件。
3.   hello.s:编译后的汇编文件。
4.   hello.o:汇编后的可重定位文件。
5.   hello:链接后的可实行文件。
6.   hello.o.txt:hello.o反汇编得到的文本文件。
7.   hello.txt:hello反汇编得到的文本文件。
1.4 本章小结

       本章介绍了Hello的P2P过程、020过程,详细介绍了完资源论文的所用的软件环境、硬件环境、开发与调试工具,并罗列出了完资源论文的中心效果文件及其作用。



第2章 预处理

2.1 预处理的概念与作用

概念:预处理(preprocessing)指的是预处理器(cpp)根据以字符#开头的命令(如#include、#define、#pragma等),修改原始的C程序,末了生成.i文本文件的过程。
作用:

  • 预处理会将全部的#define删除,并且睁开全部的宏界说。
  • 处理全部的条件预编译指令,比如#if、#ifdef、#elif、#else、#endif等。
  • 处理#include预编译指令,将被包罗的文件直接插入到预编译指令的位置。
  • 删除全部的解释。
  • 添加行号和文件标识,以便编译时产生调试用的行号及编译错误警告行号。
  • 生存全部的#pragma编译器指令
公道使用预处理功能编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。
2.2在Ubuntu下预处理的命令

预处理命令:gcc -m64 -no-pie -fno-PIC -E hello.c -o hello.i
注意这里-m64是64位程序编译,-no-pie是为了防止生成地点无关程序,-fno-PIC是为了防止生成位置无关代码使文件变为动态对象从而使得后面无法通过静态链接生成可实行文件。(后面这边部门就不会再重复了)(附:实在刚开始做时忘记加这些,效果中心从编译之后生成的都有很多差别,比如汇编代码中的函数会跟上@PLT,中心会出现32位寄存器,在查看ELF头等的类型时是动态的,另有无法静态链接等)

图 2 预处理命令


图 3 预处理文件
输入命令后,文件夹出现hello.i文件

2.3 Hello的预处理效果解析

分析hello.i可以发现:程序中的解释被删除,而#include所包罗的头文件被换成相应的代码;hello.c源程序包罗解释仅有23行,但hello.i有3092行。其中包罗大量的typedef,如typedef unsigned char __u_char;还包罗600多行的枚举(enum)类型,以及标准C的函数原型,如extern int printf (const char *__restrict __format, …);标准输入输出和错误也在这里被界说(extern FILE *stdin;extern FILE *stdout;extern FILE *stderr;);源程序代码被放在了.i文件的末了。
2.4 本章小结

本章我们介绍了hello.c的预处理过程,大抵分析了预处理后形成的hello.i文件。可以知道,仅20多行的.c文件预处理后的文件竟有3000多行。虽然产生的hello.i文件具有可以或许独立运行的一套源代码,而不是实现功能的代码片断,但如果编写一个hello程序必要3000行,如许的服从是极其低下的。这也就是预处理的意义:能让我们轻松写出可读性高,方便修改,利于调试的代码。


第3章 编译

3.1 编译的概念与作用

概念:编译是将预处理后的文本文件.i翻译为ASCII汇编语言的文本文件.s。编译程序把一个源程序翻译成目的程序的工作过程分为五个阶段:词法分析,语法分析,语义检查和中心代码生成,代码优化,目的代码生成。重要是举行词法分析和语法分析,又称为源程序分析,分析过程中发现有语法错误,给出提示信息。

作用:将高级程序语言翻译为统一的,靠近呆板语言,对呆板友好的汇编语言。它为差别高级语言差别编译器提供了通用语言。如:C编译器和Fortran编译器产生的输出文件都是一样的汇编语言。
3.2 在Ubuntu下编译的命令

通过在终端输入gcc -S hello.i -o hello.s可以生成编译产生的.s文件。

图 4 编译命令


图 5 编译文件
可以看到在输入命令后,文件夹中出现了hello.s文件。
3.3 Hello的编译效果解析

3.1.1 数据 

  • 常量
在本程序中,常量重要是指程序中出现的两个字符串常量:
"用法: Hello 学号 姓名 秒数!\n"和"Hello %s %s\n"
在汇编文件hello.s开头的.LC0和.LC1中存放了这两个字符串。

图 6 字符串常量


我们可以看到这两个字符串常量都是存放在.rodata节中的,即只读数据。
注:
.LCn:是 Local Constant 的缩写。即指局部const常量。
.LFBn:是 Local Function Beginning 的缩写。
.LFEn:是 Local Function Ending 的缩写。
.LBBn:是 Local Block Beginning 的缩写。
.LBEn:是 Local Block Ending 的缩写。

当然,除了这两个字符串常量以外,程序中也出现了立刻数常量,这是一个很常见的,比如在调用exit(1)时:

图 7 片断1

     
                   未添加选项-m64 -no-pie -fno-PIC
         

图 8 片断2
(用作对比)

图 9 片断3
我们可以看到对于这种常量它会直接用$加数字的情势来表现。

  • 变量
在本程序中,变量只有局部变量,重要有argc和i。而我们都知道局部变量是存储在栈中的。
我们起首来看在汇编文件中是如何存储argc的,先找到与其相关的代码:

图 10 片断4


而我们再来看相关的汇编代码:

图 11 片断5

通过将一个数与4举行比较,我们可以判断-20(%rbp)中存储的即是argc,因此可以得知argc是存放在栈中位于基址%rbp下的20个字节处。
而同理i的存放也是类似的。

图 12 片断6


图 13 片断7
可以推断,在此程序中,局部变量i是存放在栈中位于基址%rbp下的4个字节处的。
3.1.2 赋值
赋值这一操作是十分常见的,而它在汇编语言中也十分简单,一般是采用movx的操作,其中x有b、w、l等,分别代表8、16、32位的长字值。当然也有其他的赋值指令,在这里就不举行叙述。

图 14 片断8

如上面这个例子,当给整型i赋值为0时,直接用movl操作即可。
3.1.3 算术操作
在本程序中,算术操作重要是加法,就拿循环中i++而言,其汇编语言如下:

图 15 片断9
可以看到,它直接用addl举行加法操作。
3.1.4 关系操作及控制转移
一般而言,关系操作和控制转移每每会一起发生,在本程序中,我们拿将argc与4比较的汇编语言举行分析。

图 16 片断10
可以看到,他用cmpl举行判断,判断完后它使用je举行控制转移,je表现若相等则跳转,但看起来实现很简单,实际上它在用cmp举行比较时,实质上是举行了减法,然后在je跳转操作中回去判断相应的标记寄存器的值,进而来实现转移,在控制转移指令中另有很多,比如jne、jmp等等,而在关系判断时,有时编译器还会用test来代替cmp比较一个数和0,从而避免了减法运算。
3.1.5 函数操作
本程序中我们重要去分析printf和exit函数,这里为什么先来分析函数操作而不是按照ppt中的先分析数组操作,是由于先通太过析函数传入参数的方式,我们后面可以更好去判断数组的相应下标。相应汇编代码如下:
分析之前,我们起首要知道,在64位系统中,传递的参数会以如下方式举行传递,%rdi %rsi %rdx %rcx %r8 %r9依次用来传递函数的前六个参数,而后面的其他参数则会用堆栈来传递。因此这里我们可以知道传给rdi的就是printf的第一个参数,即"用法: Hello 学号 姓名 秒数!\n"的字符串常量,存储在.LC0的位置;同理exit它的第一参数就是1,传递完参数后就会call举行运行;运行之后,函数的返回值会存储在rax寄存器中。
3.1.6 数组操作
有了前面的基础,我们就可以更好去分析程序在存储数组及访问数组是怎么操作的了。

图 17 片断11

     
                   未添加选项-m64 -no-pie -fno-PIC
         


图 18 片断12
这一部门是for循环内的语句,通太过析传递的参数,我们可以推断-32(%rbp)+16的位置存储的是argv[2],同理-32(%rbp)+8 -32(%rbp)+24分别存储的是argv[1] argv[3],即数组也是存储在栈中,但注意这里它的大小是一个char指针,在64位系统中即8个字节,故在栈中每8个存储一个数。
3.4 本章小结

本节重要介绍编译器ccl通过编译由.i文件生成汇编语言的.s文件的过程,并分析了常量,变量,赋值,函数等各类C语言的数据与操作的汇编表现。汇编语言和高级语言很差别,纵然是高级语言中一个简单的条件语句或者循环语句在汇编语言中都必要涉及到更多步调来实现。学习汇编语言与编译,使我们可以或许真正的明白计算机底层的一些实行方法,有利于我们以后对程序的优化或调试。可以这么说,编译器简直就是艺术。

第4章 汇编

4.1 汇编的概念与作用

       所谓汇编,就是汇编器 (as) 将 hello.s 翻译成呆板语言指令,把这些指令打包成可重定位目的程序的格式,并将效果生存在文件 hello.o 中,因此hello.o 是一个二进制文件,而汇编也就是从人可以或许读懂的字符翻译成为cpu可以或许读懂的二进制程序码的过程。
当编译器将c源代码一起翻译成汇编代码之后,仍然不是及其可以读懂的格式。cpu在运行程序时通过呆板码来判断所要实行的指令,因此还必要将ascii格式的汇编代码转化为呆板码。
但必要注意的是,汇编仍然是一个中心过程。我们所编写的程序包罗着在外部的库中界说的函数,同时也缺少从系统进入程序的中心函数。
更进一步,当代码越写越大之后,可能会出现更多的界说和引用分离的环境,例如一个函数在一个.c源文件中界说,而被另一个.c文件中的函数引用。在这种环境下,预处理到编译,不过是将单个的.c文件举行了翻译。
要想程序完备可用,还必要一个将多个文件归并成一个完备的可实行文件的过程,这个过程就是链接,而汇编就是在文件中根据汇编代码生成一些可以或许指引链接过程举行的数据布局。
形象的说,我们已经造好了一台汽车的全部零件,在将零件组装起来之前,我们如今要做的就是打造螺丝钉。
4.2 在Ubuntu下汇编的命令

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

图 19 汇编命令


图 20 汇编文件
可以看到生成了hello.o文件
4.3 可重定位目的elf格式

hello.o 文件在 x86-64 Linux 和 Unix 系统中使用可实行可链接格式即 (ELF),典型的 elf 可重定位目的文件格式如下:

图 21 elf可重定位目的文件

接下来使用readelf来分析其中一些节,如ELF头,节头部表,符号表,另有重定位条目,其中的其他节在反汇编中会表现。
4.3.1 ELF
      使用readelf -h hello.o查看ELF

图 22 ELF头

EFL 头以 16 字节的序列 Magic 开始,这个序列描述了生成该文件的系统的字的大小和字节次序,ELF 头剩下的部门包罗帮助链接器语法分析和表明目的文件的信息,其中包罗 ELF 头的大小、目的文件的类型、呆板类型、字节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量等信息。我们可以看到它的类型是REL,及可重定位目的文件。

EFL头中还包罗程序的入口点地点,也就是程序运行时要实行的第一条指令的地点为0x0,而通过后面的反汇编效果我们也可以举行验证:

图 23 反汇编文件

4.3.2 节头部表
      使用 readelf -S hello.o 命令查看节头部表(在节头部表中静态和动态的基本雷同,故不多做叙述)

图 24 节头部表

节头部表其描述了每个节的语义,包罗每个节的类型,地点,大小等(这里由于用的是中文包,可能有些翻译不太合适,如flags中声明的读写权限等)。
     

图 25 节头部表权限关键词

4.3.3 符号表
使用 readelf -s hello.o 命令查看 .symtab 节中的 ELF 符号表

图 26 符号表

在符号表的这些条目中,value指的是该符号距界说目的的节的起始位置的偏移,size表现目的的大小,如这里main函数有152个字节,type表明它的类型,如数据还是函数,亦或者是NOTYPE,Bind表现该符号是当地的还是全局的,而Ndx表现到节头部表的索引,而其中有三个伪节未出如今节头部表的,即ABS, UNDEF, COMMON,可以看到像动态链接库里的都是UNDEF,ABS表现不应被重界说的符号,COMMON表现未初始化的全局变量,在我们这里并没出现COMMON。这里我们可以看到末了的一些符号都是其他目的模块中的函数类型为UND,比如puts,exit等。
4.3.4 重定位条目
       使用 readelf -r hello.o 命令查看 hello.o 的重定位条目:


图 27 可重定位条目

当汇编器生成 hello.o 后,它并不知道数据和代码最终将放在内存中的什么位置。它也不知道这个模块引用的任何外部界说的函数或者全局变量的位置。以是,无论何时汇编器遇到对最终位置未知的目的引用,它就会生成一个重定位条目,告诉链接器在将目的文件归并成可实行文件时如何修改这个引用。
在这个条目中(由于中文包翻译不是很好,一切都按书上来),offset偏移量表现必要被修改的引用节偏移;type类型告诉了该重定位的类型,这些类型浩繁;addend表现一些重定位类型要用它来对修改引用的值举行偏移调解。
这里出现了三个类型,包罗书上提到的两种最基本的重定位类型,即R_X86_64_PC32和R_X86_64_32,当然也有一个是R_X86_64_PLT32,这一个涉及到动态链接的重定位,而且我们也看到出现这个条目的名称基本都是UND类型的符号函数,即在共享库中的函数。至于如何通过重定位条目计算地点我们后面再说。

4.4 Hello.o的效果解析

通过objdump -d -r hello.o 分析hello.o的反汇编,我们将其与第3章的 hello.s举行对照分析,进而来分析二者的差别。

图 28 hello.o反汇编文件

1) 呆板语言的构成:
      呆板语言是用二进制代码表现的计算机能直接识别和实行的一种呆板指令的聚集,因此呆板语言是有二进制码构成的。一条指令就是呆板语言的一个语句,它是一组有意义的二进制代码,指令的基本格式如,操作码字段和地点码字段,其中操作码指明白指令的操作性质及功能,地点码则给出了操作数或操作数的地点。
2)与汇编语言的映射关系:
      汇编指令是呆板指令便于影象的誊写格式。每行的操作码都与其后的汇编指令逐一对应。比如说mov指令对应的呆板码是48,%rsp与%rbp寄存器对应的呆板码分别是89和e5,当cpu读取到mov指令后,就立刻解析出这是mov指令,而如果其后面跟两个寄存器,因此又会继续读取后面的两个字节,并将其翻译成对应的寄存器,并举行操作,因此二者是逐一对应,没有二义性的。
3)操作数:
      反汇编代码中的立刻数是十六进制数,而 hello.s 文件中的数是十进制的。
      寄存器寻址两者雷同。内存引用 hello.s 中会用伪指令(如.LC0)代替,而反汇编则是基址加偏移量寻址:0x0(%rip)。
4)分支转移:
      在 hello.s 中,分支跳转的目的位置是通过 .L1、.L2 如许的助记符来实现的,而 hello.o中,跳转的目的位置是具体的数值。但注意这个数值还不是具体的一个地点,因为此时还没举行链接,它是通过重定位条目举行计算得来的,是一个相对的地点值,由于差别文件代码链接归并和,一个文件本身的代码的相对地点不会改变,以是不必要与外部重定位,而可以直接计算出具体的数值,因此这里就已经完成了全部的操作,这条语句将以这种情势加载到内存中被cpu读取与实行。
5)函数调用:
      在hello.s中,用call指令举行调用函数时,总会在call指令后直接加上函数名,而通过.o文件反汇编得到的汇编代码中,call指令后会跟着的是函数通过重定位条目指引的信息,由于调用的这些函数都是未在当前文件中界说的,以是一定要与外部链接才可以或许实行。在链接时,链接器将依靠这些重定位条目对相应的值举行修改,以保证每一条语句都可以或许跳转到正确的运行时位置。
       这里另有一个小区别是在反汇编中,它在汇编指令间会对一些符号插入可重定位条目到它的下一行。
4.5 本章小结

       汇编器对编译器生成的汇编代码文件更深一层,翻译成呆板代码文件,也就是可重定位目的文件。由于每个文件中只有一部门的函数,且文件直接相互引用,相互依赖。与此同时,对于链接器来说,每个文件不过是一个字节块,要想解决这些字节块内部之间的互联逻辑,就必要汇编器多做一些,再将汇编代码翻译成呆板代码时加入一些可以或许引导链接器举行链接的数据布局。至此,汇编器的工作就结束了,离成功不过寸步之遥。
       而在本章中,分析了汇编的过程,并分析了 ELF 头、节头部表、重定位节以及符号表。比较了 hello.s 和 hello.o 反汇编之后的代码的差别。

第5章 链接

5.1 链接的概念与作用

概念:链接是将各种代码和数据片断收集并组合成为了一个单一文件的过程,这个文件可被加载(复制)到内存并实行。

作用:链接在软件开发中扮演偏重要脚色,因为它使得分离编译成为可能。我们可以将软件举行模块化设计,然后模块化编程,如许分组工作高效。而且,必要修改或者调试时,只必要修改某个模块,然后简单地重新编译它,并重新链接,而不是全部重新编译链接。纵然对hello如许一个非常简单的小程序,链接的作用也是巨大的。
5.2 在Ubuntu下链接的命令

Linux下使用链接器 (ld) 链接的命令为:
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

图 29 链接命令

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

分析hello的ELF格式,用readelf等列出其各段的基本信息,包罗各段的起始地点,大小等信息。
在可实行目的文件中,其各类信息与在可重定位目的文件中实在是差不多的,故我们这里一些基本概念就不多叙述。

5.3.1 ELF
      查看hello的ELF头:readelf -a hello

图 30 ELF头

我们可以看到可文件的类型为可实行文件,入口点地点为0x4010f0,以及其他的基本信息。比如节头部表的偏移量,ELF头的大小等

       5.3.2 节头部表
              以下是部门的节头部表

图 31 部门节头部表

       可以看到,其基本内容和之前还是很相似的,但链接器将各个文件对应的段都归并了,并且重新分配并计算了相应节的类型、位置和大小等信息。通过节头部表我们就可以知道各个节的地点和大小了。而这些我们都可以到其反汇编文件中得到验证。
       5.3.3 符号表
              以下是部门符号表

图 32 符号表

5.3.4 段节

图 33 段节

5.3.5 Dynamic section

图 34 动态节

5.4 hello的假造地点空间

使用edb加载hello,查看本进程的假造地点空间各段信息,并与5.3对照分析阐明。  

图 35 edb内存空间截图

该图展示了edb中data dump的默认显示地点范围,是从0x401000到0x402000.然厥后看一下hello的程序头表确认它所对应的段

图 36 程序头表

程序头表描述了每个段在文件中的位置、大小以及它被放进内存后所在的位置和大小。程序头表有一列是type类型,该类型用来精确的描述各段。PHDR生存程序头表,INTERP指定程序从可实行文件映射到内存之后,必须调用的表明器(就像java必要java假造机表明一样)。这个表明器可以通过链接其他程序库,来解决符号引用。LOAD表现一个必要从二进制文件映射到假造地点空间的段,其中生存了常量数据(字符串等)、程序目的代码。DYNAMIC生存了动态链接器(前面interp指定的表明器)使用的信息。GNU_STACK:权限标记,用于标记栈是否是可实行。GNU_RELRO:指定在重定位结束之后哪些内存区域是必要设置只读。
而每一段的Filesiz关键字和Memsiz关键字给出了每一段的文件大小和占用内存的大小,PhysAddr关键字给出的是每个段的物理地点,offset是偏移值,剩下的flags,align给出的是别的的信息,如许我们就知道了一个可实行程序每个段在内存中的位置和其所占空间的大小。
而通太过析,我们可以知道第二个LOAD段是代码段,有读和实行权限,开始于内存地点0x401000处,统共的内存大小为0x27d字节,并且被初始化为可实行目的文件的头也是0x27d字节,其中包罗ELF头,程序头部表以及.init、.text和.rodata节。
第四个LOAD段是数据段,有读和写权限,开始于内存地点0x403e50处,统共的内存大小为0x1fc字节,并用从目的文件中偏移0x2e10处开始的.data节中的0x1fc字节初始化。
这是我们再根据反汇编的代码和内存空间去比较发现是雷同的。

图 37 反汇编验证

              我们这里也来对比一下第四个LOAD段的内存空间来看看

图 38 edb内存查看
       可以看到在0x403e50以前存储的都是0,而在之后存储了一些内容,当然我们这里也很难知道存储的是什么,但能阐明简直是从这一段开始的,这验证了我们的想法。
5.5 链接的重定位过程分析

objdump -d -r hello 分析hello与hello.o的差别,阐明链接的过程。

  • hello新增了很多的函数相关的代码

图 39  hello反汇编文件

这是通过重定位实现的。

  •  新增了节
我们可以看到hello显着多了很多其他的节,比如.init .plt等。

图 40 反汇编新增节

       init是为了初始化程序,而plt是由于共享库链接时生成的。

  •  函数跳转转移
在函数调用时,由于hello是重定位后的可实行程序,因此其调用函数时,所调用的地点就是函数所在简直切地点,而hello.o则是调用一些相对的地点。

  •  常量,全局变量的调用
在将字符串常量作为参数传递时,我们可以看到它直接使用了其地点来读取,而在hello.o则是要通过重定位来实现。

结合hello.o的重定位项目,分析hello中对其怎么重定位的。
我们重要拿书上提到的两种重要重定位类型举行分析。
第一种是重定位的绝对引用,这种相对简单。

图 41 可重定位条目分析
这是在hello.o中传递字符串常量时出现的重定位条目(第一个),我们可以看到mov指令开始于节偏移0x19的位置,包罗1字节的操作码0xbf,后面跟着对该字符串常量地点的32位绝对引用的占用符。通过重定位条目我们可以知道,它的引用符号为.rodata,addend为0。下面我们去看.rodata中的地点。

图 42 .rodata验证

       上图的信息量很大,我们起首通过readelf -x .rodata hello去查看hello的.rodata段的十六进制情势,可以看到是从0x402000开始的,但为什么不是下面的效果不是0x402000呢?这一点我也不太确定,但我们可以看到从0x402008开始有大量的信息存储,比如e794a8e6我们去查hello.s中看到的赋值序列,上网查询得知,这是汉字的八进制转义序列,每三个作为一个编码,转换为十六进制可以发现二者恰好对应,阐明从0x402008开始存储的正是该字符串常量。

图 43 反汇编验证

       通过查询可以确认是正确的。
       而第二种相对寻址该程序中并未表现,因为它只有一个目的模块。
5.6 hello的实行流程

使用edb实行hello,阐明从加载hello到_start,到call main,以及程序停止的全部过程。请列出其调用与跳转的各个子程序名或程序地点。
由于对edb操作不是很认识,我采用的是gdb的方式,我们起首通过前面说到的入口点,即_start处的地点0x4010f0,在此处设置断点

图 44 gdb设置断点

之后我们单步开始运行,然后看它跳转的函数及其地点。
程序名
程序地点
_start
0x4010f0
_libc_start_main_impl
0x7ffff7c29dc0
__GI___cxa_atexit
0x7ffff7c458c0
__new_exitfn
0x7ffff7c456d0
_init
0x401000
_dl_audit_preinit@plt
0x7ffff7c286d0
(跳至一个空的,没名字的地方)
0x7ffff7c28350
同上
0x7ffff7c28000
_dl_runtime_resolve_xsavec
0x7ffff7fd8d30
_dl_fixup
0x7ffff7fd5e70
_dl_lookup_symbol_x
0x7ffff7fcf0d4
do_lookup_x
0x7ffff7fce3f0
check_match
0x7ffff7fce24x
strcmp
0x7ffff7fea224
之后开始不断返回直到_dl_audit_preinit

__libc_start_call_main
0x7ffff7c29d10
_setjmp
0x7ffff7c421e0
_sigsetjmp
0x7ffff7c42110
__sigjmp_save
0x7ffff7c42190
main
0x401125
printf
0x4010a0
atoi
0x401191
sleep
0x4010e0
getchar
0x4010b0
返回至__libc_start_call_main

__GI_exit
0x7ffff7c455f0
__run_exit_handlers
0x7ffff7c45390
__GI___call_tls_dtors
0x7ffff7c45d60
_dl_fini
0x7ffff7fc9040
___pthread_mutex_lock
0x7ffff7c97ef0
_dl_audit_activity_nsid
0x7ffff7fde250
_dl_sort_maps
0x7ffff7fd6730
_fini
0x4011b4
_dl_audit_objclose
0x7ffff7fde570
最终跳至__GI_exit退出程序


整个过程实在调用了很多未知的函数,但总结来说过程是
_start -> __libc_start_main ->  init -> main -> exit.
5.7 Hello的动态链接分析

分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识阐明。
动态链接是一项有趣的技能。让我们思量一个简单的事实,printf,getchar如许的函数着实使用的太过频繁,因此如果每个程序链接时都要将这些代码链接进去的话,一份可实行目的文件就会有一份printf的代码,这是对内存的极大浪费。为了遏制这种浪费,对于这些使用频繁的代码,系统会在可重定位目的文件链接时仅仅创建两个辅助用的数据布局,而直到程序被加载到内存中实行的时候,才会通过这些辅助的数据布局动态的将printf的代码重定位给程序实行。即是说,直到程序加载到内存中运行时,它才知晓所要实行的代码被放在了内存中的哪个位置。
这种有趣的技能被称为延迟绑定,将过程地点的绑定推迟到第一次调用该过程时。而那两个辅助的数据布局分别是过程链接表(PLT)和全局偏移量表(GOT),前者存放在代码段,后者存放在数据段。

图 45 节头部表查看.got节
       我们起首通过前面的节头部表得到got的地点,
       可以看到.got的位置是0x403ff0,我们去查相应的内存空间。

图 46 运行前got内存空间

       可以看到在运行之前是没什么内容的,而当我们运行之后,我们来看看它的变化:

图 47 运行后got内存空间

       通过前后对比:在实行_dl_init之前,.got是全0的;在实行_dl_init之后,.got变成了相应的值。因此推测_dl_init作用是:初始化程序,给其赋上调用的函数的地点,使这些被调用的函数链接到了动态库。
5.8 本章小结

本章介绍了链接的概念与内容,分析了hello的elf格式,查看了hello的假造地点空间,对链接的重定位过程举行了分析,并验证了其重定位条目的引用等,并使用gdb和edb分析了hello的实行流程和动态链接分析。
在本章中呢,链接器ld通过可重定位目的文件中的数据布局,解析每个文件中的符号,仔细比对了符号的界说和引用,最终为每个符号的引用都找到了正确的符号界说的位置。而重定位的过程更加必要胆小如鼠,链接器必要在特定的位置修改值,使得程序在运行时可以或许指哪打哪而不会弊端。毕竟在cpu中哪怕是一个字节的弊端,失之毫厘,差之千里。因此可以说,链接的过程同样是布满着各种艰难困苦的。

第6章 hello进程管理

6.1 进程的概念与作用

概念:进程的经典界说是一个实行中程序的实例,系统的每个程序都运行在某个进程的上下文。上下文是由程序正确运行所需的状态构成的,这个状态包罗存放在内存里的程序的代码和数据,它的栈,通用目的寄存器的内容,程序计数器,环境变量以及打开文件描述符的聚集。进程是计算机科学中最深刻,最成功的概念。
作用:通过进程,我们会得到一种假象,似乎我们的程序是当前唯一运行的程序,我们的程序独占处理器和内存,我们程序的代码和数据似乎是系统内存中唯一的对象。而这些假象就是通过进程来实现的。
6.2 简述壳Shell-bash的作用与处理流程

Shell 是一种交互型的应用级程序,用户可以或许通过 Shell 与操作系统内核举行交互。
bash,全称为Bourne-Again Shell。它是一个为GNU项目编写的Unix shell。bash脚本功能非常强盛,尤其是在处理自动循环或大的使命方面可节流大量的时间。bash是很多Linux平台的内定Shell。

图 48 用户内核层次图
处理流程:

  • Shell读取用户输入的命令。
  • Shell判断是否为shell内置命令,如果不是则以为是一个可实行文件。、
  • Shell构建参数argv和环境变量envp用作execve加载时的参数。
  • Shell通过fork创建子进程,再通过execve函数加载可实行文件,并跳转到程序的入口点实行程序。
  • 当程序停止或停止后,Shell采取创建的子进程,之后再不断循环。
6.3 Hello的fork进程创建过程

我们在Shell上输入./hello,这个不是一个内置的Shell命令,以是Shell会以为hello是一个可实行目的文件,通过调用某个驻留在存储器中被称为加载器的操作系统代码来运行它。当Shell运行一个程序时,父进程通过fork函数生成这个程序的进程。这个子进程几乎与父进程雷同,子进程得到与父进程雷同的假造地点空间(独立)的一个副本,包罗代码,数据段,堆,共享库以及用户栈,并且和父进程共享文件。它们之间的区别是PID差别。
通过查阅资料我们来具体讲讲fork的实现:
Linux 通过 clone() 系统调用来实现 fork(),由于 clone() 可以自主选择必要复制的资源,以是这个系统调用必要传入很多的参数标记用于指明父子进程必要共享的资源。fork(),vfork(),__clone() 函数都必要根据各自传入的参数去底层调用 clone() 系统调用,然后再由 clone() 去调用 do_fork()。do_fork() 完成了创建的大部门工作,该函数调用 copy_process() 函数,然后让进程开始运行。
copy_process() 函数是核心,其工作分为这几步:

  • 调用 dup_task_struct() 为新进程创建一个内核栈,thread_info 布局和 task_struct,这些值和当前进程的值雷同。也就是说,当前子进程和父进程的进程描述符是一致的。
  • 检查一次,确保创建新进程后,拥有的进程数目没有超过给它分配的资源和限制。全部进程的 task_struct 布局中都有一个数组 rlim,这个数组中记载了该进程对占用各种资源的数目限制,以是如果该用户当前拥有的进程数目已经到达了峰值,则不答应继续 fork()。这个值为 PID_MAX,大小为 0x8000,也就是说进程号的最大值为 0x7fff,即短整型变量 short 的大小 32767,其中 0~299 是为系统进程(包罗内核线程)生存的。
  • 子进程为了将自己与父进程区分开来,将进程描述符中的很多成员全部清零或者设为初始值。不过大多数数据都未修改。
  • 将子进程的状态设置为 TASK_UNINTERRUPTIBLE 深度睡眠,不可被信号叫醒,以保证子进程不会投入运行。
  • copy_process() 函数调用 copy_flags() 以更新 task_struct 中的 flags 成员。其中表现进程是否拥有超级用户管理权限的 PF_SUPERPRIV 标记被清零,表现进程还没有调用 exec() 函数的 PF_FORKNOEXEC 标记也被清零。
  • 调用 alloc_pid 为子进程分配一个有用的 PID
  • 根据传递给 clone() 的参数标记,调用 do_fork()->copy_process() 拷贝或共享父进程打开的文件,信号处理函数,进程地点空间和命名空间等。一般环境下,这些资源会给进程下的全部线程共享。
  • 末了,copy_process() 做扫尾工作并返回一个指向子进程的指针。
6.4 Hello的execve过程

execve() 函数加载并运行可实行目的文件,且带参数列表 argv 和环境变量列表 envp,execve() 函数调用一次从不返回。它的实行过程如下:

  • 删除已存在的用户区城。删除当前进程假造地点的用户部门中的已存在的区域布局。
  • 映射私有区城。为新程序的代码、数据、bss 和栈区域创建新的区域布局。全部这些新的区域都是私有的、写时复制的。代码和数据区域被映射为 hello 文件中的.text 和.data 区。bss 区域是请求二进制零的,映射到匿名文件,其大小包罗在 hello 中。栈和堆区域也是请求二进制零的,初始长度为零。图 6.3 概括了私有区域的差别映射。
  • 映射共享区城。如果 hello 程序与共享对象(或目的)链接,比如标准C 库 libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户假造地点空间中的共享区域内。
  • 设置程序计数器(PC)。execve 做的末了一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点,也就是_start 函数的地点。下一次调理这个进程时,它将从这个入口点开始实行。Linux 将根据必要换入代码和数据页面。_start 函数调用系统启动该函数__libc_start_main。它初始化实行环境,调用用户层的 main 函数,处理 main 函数的返回值,并且在必要的时候把控制返回到内核。

6.5 Hello的进程实行

6.5.1 逻辑控制流
操作系统将一个 CPU 物理控制流,分成多个逻辑控制流,每个进程独占一个逻辑控制流。当一个逻辑控制流实行的时候,其他的逻辑控制流可能会临时停息实行。一般来说,每个逻辑控制流都是独立的。当两个逻辑控制流在时间上发生重叠,我们说是并行的。如图:

图 49 逻辑控制流

处理器在多个进程中来回切换称为多使命,每个时间当处理器实行一段控制流称为时间片。因此多使命也指时间分片。
6.5.2 用户模式和内核模式
为了限制一个应用可以实行的指令以及它可以访问的地点空间范围,处理器用一个控制寄存器中的一个模式位来描述进程当前的特权。

当设置了模式位时,进程就运行在内核模式。一个运行在内核模式的进程可以实行指令集中的任何指令,并且可以访问系统中的任何内存位置。
没有设置模式位时,进程就运行在用户模式。用户模式中的进程不答应实行特权指令,比如停止处理器、改变模式位,或者发起一个 I/O 操作。也不答应用户模式的进程直接引用地点空间中内核区内的代码和数据。用户程序必须通过系统调用接口间接地访问内核代码和数据。
进程从用户模式变为内核模式的唯一方法是通过诸如中断、故障或者陷入系统调用如许的异常。当异常发生时,控制传递到异常处理程序,处理器将模式从用户模式变为内核模式。处理程序运行在内核模式中,当它返回到应用程序代码时,处理器就把模式从内核模式改回到用户模式。
6.5.3 上下文切换
操作系统内核为每个进程维护一个上下文。所谓上下文就是内核重新启动一个被抢占的进程所需的状态。它由一些对象的值构成,这些对象包罗通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据布局,比如描述地点空间的页表,包罗有关当前进程信息的进程表,以及包罗进程已打开文件的信息的文件表。
6.5.4 hello 的实行
从 Shell 中运行 hello 时,它运行在用户模式,运行过程中,内核不断切换上下文,使运行过程被切分成时间片,与其他进程交替占用实行,实现进程的调理。如果在运行过程中收到信号等,那么就会进入内核模式,运行信号处理程序,之后再返回用户模式。
6.6 hello的异常与信号处理

我们来结合hello的实行过程具体分析会出现哪几类异常,会产生哪些信号,又怎么处理的。
6.6.1 异常
异常可以分为四类:中断、陷阱、故障和停止。我们结合hello的实行过程具体来阐明。
中断:中断一般是来自处理器外部的I/O装备的信号的效果。比如在 hello 运行过程中,我们敲击键盘,那么就会触发中断,系统调用内核中的中断处理程序实行,然后返回,返回到hello的下一条指令继续实行。

陷阱:陷阱是有意的异常,一般是用来提供用户程序和内核之间的接口,即系统调用。我们的 hello 运行在用户模式下,无法直接运行内核中的程序,比如像 fork,exit 如许的系统调用。于是就通过陷阱的方式,实行 systemcall 指令,内核调用陷阱处理程序来实行系统调用。

故障:故障是有一些错误环境引起的,当故障发生时,处理器会将控制转移给故障处理子程序,若能修正,则返回到原指令重新实行,否则就会停止。一个经典的故障是缺页异常,比如当我们的 hello 运行时,当某一条指令引用一个假造地点,而地点相对应的物理页面不在内存中,就会发生故障。内核调用故障处理程序(这里是缺页处理程序),缺页处理程序从磁盘中加载适当的页面,然后将控制返回给引起故障的指令,该指令就能没有故障地实行了。
当然,在hello的实行过程中也有一些故障会使程序直接停止。
停止:hello 在运行时,也有可能遇到硬件错误,比如说DRAM或者SRAM位被破坏时发生的奇偶错误,这时处理程序会停止hello程序。

6.6.2 信号
信号,是一种更高层的软件情势的异常,它答应进程和内核中断其他进程。Linux中的信号有很多种,我们可以用kill -l命令查看。

图 50 信号类型

接下来,我们通过在程序运行时通过按键盘,从键盘发送信号,以及输入各种命令对于程序运行进程中的异常与信号的处理做一个更深入的了解。



  • 不停乱按键盘

图 51 测试1


图 52 测试2

       可以看到,在程序运行过程中,只要hello不停止,我们输入的命令会显示在屏幕上,但是Shell不会在显示运行hello的过程运行他们,而是类似将它们放入缓冲区,当程序运行后,它会像运行其他命令一样对它们举行解析,正常运行相应的命令。



  • Ctrl-Z

图 53 测试3

       按下Ctrl-Z后我们可以发现它显示该进程已停止,但它实在并未停止,它被Shell放在后台中,我们可以用一些指令查看。
-ps

图 54 测试4
       可以看到hello的相关进程信息。
-jobs

图 55 测试5
-pstree

图 56 测试6

       输入pstree可以看到它和别的进程的一些关系。
-fg

图 57 测试7
       可以看到程序又正常运行。
-kill

图 58 测试8
       我们输入kill %1表现停止第一个作业,我们再用jobs查看可以看到该作业已停止。
       ·Ctrl-C
       当hello在前台运行时,按下Ctrl-C会发送SIGINT信号,进而使进程停止。
6.7本章小结

本章了解了hello进程的实行过程。在hello运行过程中,内核对其调理,异常处理程序为其将处理各种异常。每种信号都有差别的处理机制,对差别的shell命令,hello也有差别的相应效果。可以说,进程管理就是为了约束程序的运行而存在的。

第7章 hello的存储管理

7.1 hello的存储器地点空间

起首我们结合hello程序来阐明逻辑地点、线性地点、假造地点、物理地点的概念。hello程序颠末反汇编得到的内存地点是逻辑地点,必要转换成线性地点,再颠末MMU(CPU中的内存管理单位)转换成物理地点才可以或许被访问到。
逻辑地点:包罗在呆板语言中用来指定一个操作数或一条指令的地点。每一个逻辑地点都由一个段(segment)和偏移量(offset)构成,偏移量指明白从段开始的地方到实际地点之间的间隔。通俗的说:逻辑地点是给程序员设定的,底层代码是分段式的,代码段、数据段、每个段最开始的位置为段基址,放在如CS、DS如许的段寄存器中,再加上偏移,如许构成一个完备的地点。
另一些说法:逻辑地点(Logical Address是指由程序产生的与段相关的偏移地点部门。例如,举行C语言指针编程中,可以读取指针变量本身值(&操作),实际上这个值就是逻辑地点,它是相对于你当前进程数据段的地点,反面绝对物理地点干系。只有在Intel实模式下,逻辑地点才和物理地点相等(因为实模式没有分段或分页机制,Cpu不举行自动地点转换);逻辑也就是在Intel 掩护模式下程序实行代码段限长内的偏移地点(假定代码段、数据段如果完全一样)。应用程序员仅需与逻辑地点打交道,而分段和分页机制对您来说是完全透明的,仅由系统编程职员涉及。应用程序员虽然自己可以直接操作内存,那也只能在操作系统给你分配的内存段操作。

线性地点(Linear Address) 是逻辑地点到物理地点变换之间的中心层。程序代码会产生逻辑地点,或者说是段中的偏移地点,加上相应段的基地点就生成了一个线性地点。如果启用了分页机制,那么线性地点可以再经变换以产生一个物理地点。若没有启用分页机制,那么线性地点直接就是物理地点。Intel 80386的线性地点空间容量为4G(2的32次方即32根地点总线寻址)。

假造地点:掩护模式下,hello 运行在假造地点空间中,它访问存储器所用的逻辑地点。

物理地点:计算机系统的主存被组织成一个由M个连续的字节大小的单位构成的数组,每一个字节单位给以一个唯一的存储器地点,称为物理地点,指出现CPU外部地点总线上的寻址物理内存的地点信号,是地点变换的最终效果地点。
7.2 Intel逻辑地点到线性地点的变换-段式管理


在 Intel 平台下的实模式中,逻辑地点为:CS:EA,CS 是段寄存器,将 CS 里的值左移四位,再加上 EA 就是线性地点。
而在掩护模式下,要用段描述符作为下标,到 GDT(全局描述符表)/LAT(局部描述符表)中查表得到段地点,段地点+偏移地点就是线性地点。一个逻辑地点是由段选择符和偏移地点构成的。段选择符存放在段寄存器(16 位)。前 13 位表现其段描述符在对应描述符表的索引号,后面 3 位分别是TI和RPL,TI用于阐明是GDT还是LDT,RPL用于阐明特权等级。如图是段选择符各字段含义:

图 59 段选择符各字段
其中,从段描述符中可以得到:
(1)段的基地点(Base Address):在线性地点空间中段的起始地点。
(2)段的界限(Limit):表现在逻辑地点中,段内可以使用的最大偏移量。
(3)段的属性(Attribute):表现段的特性。例如,该段是否可被读出或写入,或者该段是否作为一个程序来实行,以及段的特权级等。
从段描述符和偏移地点得到线性地点的过程如图:

图 60 逻辑地点转为线性地点

给定一个完备的 48 位逻辑地点[段选择符(16 位):段内偏移地点(32 位)],起首看段选择符的T1=0还是1,是0要转换 GDT 中的段,是1转换 LDT中的段,再根据相应寄存器,得到其地点和大小。我们就有了一个段描述符表了。然后拿出段选择符中前13位,可以在段描述符表中通过索引,查找到对应的段描述符,如许,就得到了32位段基地点。末了把32位段基地点和32位段内偏移量相加,就是要转换的线性地点了。
7.3 Hello的线性地点到物理地点的变换-页式管理

7.3.1 Intel的页式管理页式

内存管理单位负责把一个线性地点转换为物理地点。从管理和服从的角度出发,线性地点被分别成固定长度单位的数组,称为页(page)。例如,一个32位的呆板,线性地点可以到达4G,每页4KB,如许,整个线性地点就被分别为2^20页,称之为页表,该页表中每一项存储的都是物理页的基地点。为了可以或许尽可能的节流内存,CPU在页式内存管理方式中引入了多级页表的方式。
VM 系统将假造内存分割为成为假造页的大小固定的快,物理内存也被分割为物理页,成为页帧。假造页面就可以作为缓存的工具,被分为三个部门:
未分配的:VM 系统还未分配的页
已缓存的:当前已缓存在物理内存中的已分配页

图 61 假造内存分配

未缓存的:未缓存在物理内存的已分配页

7.3.2 hello的线性地点到物理地点的变换

从线性地点到物理地点的变换,我们以资料中的系统为例,

图 62 线性地点到物理地点变换

据以下步调举行转换:

  • 从cr3中取出进程的页目录地点(操作系统负责在调理进程的时候,把这个地点装入对应寄存器);
  • 根据线性地点前十位,在数组中,找到对应的索引项,因为引入了二级管理模式,页目录中的项,不再是页的地点,而是一个页表的地点。(又引入了一个数组),页的地点被放到页表中去了。
  • 根据线性地点的中心十位,在页表(也是数组)中找到页的起始地点;
  • 将页的起始地点与线性地点中末了12位相加,得到最终我们想要的地点。

其中,这里CR3是页目录基址寄存器,用于生存页目录表的起始地点。
7.4 TLB与四级页表支持下的VA到PA的变换

       如图是Core i7在TLB和四级页表支持下从假造地点VA到物理地点PA的转换:

图 63 假造地点翻译过程

TLB(Translation Lookaside Buffer,翻译后备缓冲器)俗称快表,用于加快地点翻译。每次CPU产生一个假造地点,就必须查询PTE(页表条目),在最差环境下它在内存中读取PTE,耗费几十到几百个周期。TLB是一个对PTE的缓存,每当查询PTE时,MMU先询问TLB中是否存有该条目,若有,它可以很快地得到效果;否则,MMU必要按照正常流程到高速缓存/内存中查询PTE,把效果生存到TLB中,末了在TLB中取得效果。
多级页表用于减少常驻于内存中的页表大小。由于在同一时间并非全部假造内存都被分配,那么操作系统可以只记录那些已被分配的页来减小内存开销,这通过多级页表实现。对于一个k(k>1)级页表,假造地点被分为k个假造页号和一个假造页偏移量。为了将假造地点转换为物理地点,MMU起首用第一段假造页号查询常驻于内存的一级页表,获取其二级页表的基址,再用第二段假造页号查询三级页表的基址,直到对第k级页表返回物理地点偏移,MMU就得到了该假造地点对应的物理地点。对于那些没有分配的假造地点,对应的多级页表根本不存在,只有当分配到它们时才会创建这些页表。因此,多级页表可以或许减少内存需求。
7.5 三级Cache支持下的物理内存访问

得到物理地点后,将物理地点分为 CT(标记位)、CI(组索引) 和 CO(块偏移)。根据 CI 查找 L1 缓存中的组,依次与组中每一行的数据比较,有用位有用且标记位一致则命中。如果命中,直接返回想要的数据。如果不命中,就依次去 L2、L3 缓存判断是否命中,命中时将数据传给 CPU 同时更新各级缓存。
具体过程如下:
1. 先在第一级缓存中探求要找的数据,使用TLBI找到TLB中对应的组,再比较TLBT,若雷同且有用为为1,则要找的数据就是该组的数据。
2. 否则,在第二级缓存中探求,找到后必要再将其缓存在第一级,若有空闲块,则放置在空闲块中,否则根据更换策略选择捐躯块。
3. 否则,在第三级缓存中探求,找到后必要缓存在第一,二级,若有空闲块,则放置在空闲块中,否则根据更换策略选择捐躯块。
4. 否则,在第四级缓存中探求,找到后必要缓存在第一,二,三级,若有空闲块,则放置在空闲块中,否则根据更换策略选择捐躯块。
7.6 hello进程fork时的内存映射

在 Shell 输入命令行后,内核调用fork创建子进程,为 hello 程序的运行创建上下文,并分配一个与父进程差别的PID。通过 fork 创建的子进程拥有父进程雷同的区域布局、页表等的一份副本,同时子进程也可以访问任何父进程已经打开的文件。当 fork 在新进程中返回时,新进程如今的假造内存刚好和调用 fork 时存在的假造内存雷同,当这两个进程中的任一个厥后举行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地点空间。

图 64 一个私有的写时复制对象

7.7 hello进程execve时的内存映射

execve() 函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包罗在可实行目的文件 hello 中的程序,用 hello 程序有用地替代了当出息序。加载并运行 hello 必要以下几个步调:

  • 删除已存在的用户区域,删除当前进程假造地点的用户部门中的已存在的区域布局。
  • 映射私有区域,为新程序的代码、数据、bss 和栈区域创建新的区域布局,全部这些新的区域都是私有的、写时复制的。代码和数据区域被映射为 hello 文件中的 .text 和 .data 区,bss 区域是请求二进制零的,映射到匿名文件,其大小包罗在 hello 中,栈和堆地点也是请求二进制零的,初始长度为零。
  • 映射共享区域, hello 程序与共享对象 libc.so 链接,libc.so 是动态链接到这个程序中的,然后再映射到用户假造地点空间中的共享区域内。
  • 设置程序计数器(PC),execv() 做的末了一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。

图 65 加载时内存空间

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

正如前面讲到的一样,当MMU试图翻译某个假造地点A时,如果在TLB中找不到相应的页表项,阐明发生了一个缺页故障异常,进而导致控制转移到内核的缺页处理子程序,而在处理程序中,它并不是直接到磁盘中将相应页面更换进来的,它会举行以下操作:

  • 判断该假造地点A是否合法,即判断它是否在hello程序中的某个假造内存区域中,如果不是则触发一个段错误,停止程序。
  • 判断该假造地点举行的内存访问是否合法,第一部已经判断了A是在某个区域中的,但接下来要判断实行hello程序的进程是否有权限访问A所在的这个区域,如果不合法,则触发一个掩护异常,停止进程。
  • 此时,内核已经知道该缺页是由于对合法假造地点的翻译导致的,那么它会去判断缓存是否已满,若已满还会去选择一个捐躯页,然后去更新页表,末了再重新实行导致缺页的指令。

图 66 linux缺页处理

7.9动态存储分配管理

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

当程序运行时,如果必要额外的假造内存时,可以用动态内存分配器来申请内存。动态内存分配器维护着一个进程的假造内存区域,称为堆(heap),假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地点)。
对于每个进程,内核维护着一个变量 brk,它指向堆的顶部。分配器将堆视为一组差别大小的块(block)的聚集来维护。每个块就是一个连续的假造内存片(chunk),要么是已分配的,要么是空闲的。已分配的块显式地生存为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被开释,这种开释要么是应用程序显式实行的,要么是内存分配器自身隐式实行的。

隐式空闲链表分配中,内存块的基本布局如下:

图 67 使用边界标记的堆块的格式
而对于显式空间链表,真实的操作系统实际上使用的是显示空闲链表管理。它的思绪是维护多个空闲链表,每个链表中的块有大抵相等的大小,分配器维护着一个空闲链表数组,每个大小类一个空闲链表,当必要分配块时只必要在对应的空闲链表中搜刮就好了。
其一种实现的基本布局如下:

图 68 显示空间链表基本布局
7.10本章小结

       本章介绍了当代操作系统的魂魄:存储器地点空间、段式管理、页式管理,VA 到 PA 的变换、物理内存访问, hello 进程 fork 时和 execve 时的内存映射、缺页故障与缺页中断处理、包罗隐式空闲链表和显式空闲链表的动态存储分配管理。这些巧妙的设计使得我们的 hello 最终得以运行。

第8章 hello的IO管理

8.1 Linux的IO装备管理方法

装备的模型化:文件
装备管理:unix io接口
全部的 I/O 装备(例如网络、磁盘和终端)都被模型化为文件,而全部的输入和输出都被看成对相应文件的读和写来实行。这种将装备优雅地映射为文件的方式,答应 Linux 内核引出一个简单、低级的应用接口,称为 Unix I/O,这使得全部的输入和输出都能以一种统一且一致的方式来实行
Linux的装备管理的重要使命是控制装备完成输入输出操作,以是又称输入输出(I/O)子系统。它的使命是把各种装备硬件的复杂物理特性的细节屏蔽起来,提供一个对各种差别装备使用统一方式举行操作的接口。Linux把装备看作是特别的文件,系统通过处理文件的接口—假造文件系统VFS来管理和控制各种装备。
8.2 简述Unix IO接口及其函数

8.2.1 I/O接口操作
①打开文件:一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O装备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的全部操作中标识这个文件。内核记录有关这个打开文件的全部信息。应用程序只需记住这个描述符。
②Linux shell创建的每个进程开始时都有三个打开的文件:标准输入、标准输出和标准错误。
③改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置k,初始为0.这个文件位置是从文件开头起始的字节偏移量。应用程序可以或许通过实行seek操作,显式地设置文件的当前位置为k。
④读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的文件,当k>=m时实行读操作会触发一个称为end-of-file(EOF)的条件,应用程序能检测到这个条件。在文件结尾处并没有明白的“EOF符号”。类似地,写操作就是从内存复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
⑤关闭文件:当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为相应,内核开释文件打开时创建的数据布局,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种缘故原由停止时,内核都会关闭全部打开的文件并开释它们的内存资源。(见书P622~P623)
8.2.2 I/O函数
①int open(char *filename, int flags, mode_t mode);进程通过调用open函数来打开一个已存在的文件或者创建一个新文件。open函数将filename转换为一个文件描述符,而且返回描述符数字。flags参数指明白进程打算如何访问这个文件。mode参数指定了新文件的访问权限位。
②int close(int fd);进程通过调用close函数关闭一个打开的文件。
③ssize_t read(int fd, void *buf, size_t n);应用程序通过调用read函数来实行输入。read函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表现一个错误,返回值0表现EOF。否则返回值表现的是实际传送的字节数量。
④ssize_t write(int fd, const void *buf, size_t n);应用程序通过调用write函数来实行输出。write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。(见书P624~P626)
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;

}

而这里面的vsprintf 函数将全部的参数内容格式化之后存入 buf,返回格式化数组的长度,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);

}

从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等. 这里的话syscall的实现是很复杂的,超出了我们的讨论范围,故我们就不多叙述了。
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。程序会根据每个符号所对应的ascii码,系统会从字模库中提取出每个符号的vram信息。
显卡使用的内存分为两部门,一部门是显卡自带的显存称为VRAM内存,别的一部门是系统主存称为GTT内存(graphics translation table和后面的GART含义雷同,都是指显卡的页表,GTT 内存可以就明白为必要建立GPU页表的显存)。在嵌入式系统或者集成显卡上,显卡通常是不自带显存的,而是完全使用系统内存。通常显卡上的显存访存速率数倍于系统内存,因而很多数据如果是放在显卡自带显存上,其速率将显着高于使用系统内存的环境(比如纹理,OpenGL中分平凡纹理和常驻纹理)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析

getchar 由宏实现:#define getchar() getc(stdin)。
getchar 有一个int型的返回值.当程序调用getchar时.程序就等着用户按键.用户输入的字符被存放在键盘缓冲区中.直到用户按回车为止(回车字符也放在缓 冲区中).当用户键入回车之后,getchar才开始从stdin流中每次读入一个字符.getchar函数的返回值是用户输入的第一个字符的ASCII 码,如出错返回-1,且将用户输入的字符回显到屏幕.如用户在按回车之前输入了不止一个字符,其他字符会生存在键盘缓存区中,等候后续getchar调用 读取.也就是说,后续的getchar调用不会等候用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为后,才等候用户按键.
hello 程序调用 getchar 后,它就等着键盘输入。当我们输入时,会发生异常,内核中的键盘中断处理子程序来举行处理。而异步异常-键盘中断的处理:键盘中断处理子程序。担当按键扫描码转成ascii码,生存到系统的键盘缓冲区。
getchar通过陷阱调用read系统函数,通过系统调用读取按键ascii码,直到担当到回车键才返回。此时getchar 开始从read返回的字符串中读入其第一个字符。
getchar 函数的返回值是用户输入的字符的 ASCII 码,若文件结尾则返回 -1(EOF),且将输入的字符回显到屏幕。
8.5本章小结

       IO是复杂的计算机内部与外部沟通的通道。尽管我们时时候刻都在使用着IO:通过键盘输入,通过屏幕阅读。但是系统IO实现的细节同样也是相当复杂的。
本章介绍了linux系统下的IO的基本知识,讨论了IO在linux系统中的情势以及实现的模式。然后对printf和getchar两个函数的实现举行了深入的探究。
结论

至此,hello 终于走完了它的一生,让我们为它的一生做个小结:
Hello程序的生命周期开始于程序员把其内容输入到文本编辑器中:字符数据颠末总线最终被传输到寄存器,并在文件被关闭后生存到磁盘。
接下来将源文件通过gcc编译器预处理,编译,汇编,链接,最终完成一个可以加载到内存实行的可实行目的文件。一系列操作,为hello.c一个空壳注入了活的魂魄。
接下来通过shell输入文件名,shell通过fork创建一个新的进程,然后在子进程里通过execve函数将hello程序加载到内存。假造内存机制通过mmap为hello规划了一片空间,调理器为hello规划进程实行的时间片,使其可以或许与其他进程公道使用cpu与内存的资源。此时的它,才真正成为系统中独立的个体,往回看,逻辑控制流、假造地点、malloc的高效管理、异常与信号管理,这些都为它的驰骋拥有更加广阔的天地。
然后,cpu一条一条的从hello的.text取指令实行,不断从.data段去除数据。异常处理程序监视着键盘的输入。hello里面的一条syscall系统调用语句使进程触发陷阱,内核接办了进程,然后实行write函数,将一串字符传递给屏幕io的映射文件。文件对传入数据举行分析,读取vram,然后在屏幕上将字符显示出来。可以说,Unix I/O 打开它与程序使用者交流的窗口。
直至末了hello“垂老迈矣”,运行至末了一刻,程序运行结束,__libc_start_main 将控制转移给内核,Shell 采取子进程,内核删除与它相关的全部数据布局,它在这个世界的全部陈迹至此被抹去。
从键盘上敲出hello.c的源代码程序不过几分钟,从编译到运行,从敲下gcc到终端打印出hello信息,可能甚至不必要1秒钟。
但回首这短短的1秒,却触目惊心,千难万险,其中的每一阶段无不搜集凝聚了人类几十年的智慧与心血!
高低电平传递着信息,这些信息被复杂而严谨的呆板逻辑捕捉。cpu不知疲倦的取指与实行。对于hello的实现细节,哪怕把这篇论文再扩充一倍仍讲不清晰。

附件


  • hello.c:源代码文件。
  • hello.i:预处理后的文本文件。
  • hello.s:编译后的汇编文件。
  • hello.o:汇编后的可重定位文件。
  • hello:链接后的可实行文件。
  • hello.o.txt:hello.o反汇编得到的文本文件。
  • hello.txt:hello反汇编得到的文本文件。

参考文献


  • Randal E. Bryant & David R. O’Hallaron. 深入明白计算机系统[M]. 北京:机械工业出版社,2019.
  • printf 函数实现的深入分析https://www.cnblogs.com/pianist/p/3315801.html
  •        GCC编译的四个阶段 http://t.csdnimg.cn/dMVxa
  • 深入浅出GNU X86-64 汇编
       https://www.cnblogs.com/zhumengke/articles/10643032.html

  • x86寄存器问题 http://t.csdnimg.cn/x8PaE
  • readelf命令使用阐明 http://t.csdnimg.cn/ROXm9
  • 逻辑地点、线性地点、物理地点和假造地点明白 http://t.csdnimg.cn/WvHfR
  • linux编程之main()函数启动过程
https://www.cnblogs.com/sky-heaven/p/8422023.html

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

使用道具 举报

0 个回复

倒序浏览

快速回复

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

本版积分规则

乌市泽哥

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

标签云

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