步伐人生-Hello’s P2P

打印 上一主题 下一主题

主题 1001|帖子 1001|积分 3005

摘  要

本实行从hello.c文件出发,探究其预处理、编译、汇编、链接生成可执行文件的全过程,分析每次处理前后的变化,详细展示此中间态变化;在此底子上,也讨论了hello步伐的进程管理,存储管理,IO管理方式方法,逐个分析此中的关键点,展现hello从键入shell到打印终极结果这一完备的运行周期中发生的变化。

关键词:盘算机体系;Linux;编译;链接;进程;                           

目  录

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



第1章 概述

1.1 Hello简介

From Program to Process:将一行行代码敲进文本编辑器Editor,存为hello.c文件,使用cpp预处理器处理hello.c生成hello.i文件,使用ccl编译器编译hello.i生成hello.s文件,使用as汇编器汇编hello.s生成hello.o文件,使用ld链接器将hello.o与printf.o文件链接生成hello可执行文件,如图1-1;

图1-1 From Program to Process[1]

From Zero-0 to Zero-0:输入./hello至shell,shell调用fork()生成子进程,调用execve()加载hello步伐代码,os内核为其分配捏造所在空间,MMU根据CPU传送的VA捏造所在查询TLB与内存获得PA物理所在,根据PA访问内存获得指令。运行完毕后,shell回收子步伐,开释内存空间。

1.2 环境与工具

硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上
软件环境:Windows7/10 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位/优麒麟 64位 以上;
开发工具:Visual Studio Code 64位;edb/vi/vim/gedit+gcc
1.3 中间结果

文件名

作用

hello.c

初始C代码文件

hello.i

预处理后文件

hello.s

编译后汇编语言文件

hello.o

汇编后二进制文件

hello_obj.s

hello.o反汇编文件

hello.out

链接后可执行步伐

hello_obj2.s

hello.out反汇编文件

1.4 本章小结

本章介绍了hello步伐P2P与020的大致过程,实行中的软硬件环境与开发工具,以及实行中生成的中间文件,为之后的详细讨论提供了良好的开端。

第2章 预处理

2.1 预处理的概念与作用

预处理器(cpp)根据以字符 # 开头的下令,修改原始的 C 步伐。例如 hello.c 中第 1 行的#include <stdio.h>下令告诉预处理器读取体系头文件 stdio.h 的内容,并把它直接插入步伐文本中。结果就得到了另一个 C 步伐,通常是以 .i 作为文件扩展名[1]。此处生成hello.i文件。
作用[3]:
1:将头文件中的内容(源文件之外的文件)插入到源文件中
2:举行了宏替换的过程,定义和替换了由#define指令定义的符号
3:删除掉解释的过程,解释是不会带入到编译阶段
4:条件编译
2.2在Ubuntu下预处理的下令

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

图2-1 预处理

2.3 Hello的预处理结果解析

hello.c共24行代码,hello.i共3000余行代码,观察到预处理步伐将hello.c中#include的文件代码添加至hello.i开头,而且删除了hello.c中原有的解释,如图2-2.

图2-2-a  hello.c文件


图2-2-b  hello.i文件

2.4 本章小结

本章介绍了hello.c至hello.i的预处理过程,该过程简化了步伐员编写代码的过程,使得初始代码更加简洁、易读,也为接下来的编译奠基了底子。

第3章 编译

3.1 编译的概念与作用

将C语言代码转换成CPU能够识别的二进制指令的工具叫做编译器(Compiler),编译器能够识别代码中的词汇、句子以及各种特定的格式,并将他们转换成盘算机能够识别的二进制情势,这个过程称为编译(Compile)[4]。
3.2 在Ubuntu下编译的下令

编译下令:gcc -S hello.i -o hello.s

图3-1 编译

3.3 Hello的编译结果解析

3.3.1数据
hello.s中包含常量、局部变量、全局变量。
字符串常量:
"用法: Hello 学号 姓名 手机号 秒数!\n",观察到文字被编码为3个\***;
"Hello %s %s %s\n",稳固。

图3-2 字符串常量

局部变量以立即数的情势表示,例$5,$1;

图3-3 局部变量argc存于%edi(判定argc!=5)

全局变量只有一个,即main;

图3-4 全局变量main

3.3.2赋值

图3-5 为argc赋初值并比较


图3-6 为i赋初值

3.3.3算术使用

图3-7 每次循环i+1并判定条件

3.3.4关系使用
argc与5比较如图3-5;
循环中i与9比较(源代码为i<10,此处等价转化为i<=9)如图3-7。
3.3.5数组/指针/布局使用
字符串作为变量由%rsi传递至-32(%rbp),如图3-3;之后依次将指针指向的值存至%rdx,%rsi,%rdi,它们为printf输入的三个参数。

图3-8 指针传递字符串

3.3.6控制转移

图3-9 判定if(argc!=5)


图3-10 for循环条件判定

3.3.7函数使用
初始输入值argc与argv传入main如图3-3;
printf参数传递与调用如图3-8;
别的,还调用了atoi、sleep、getchar函数,如图3-11。

图3-11 call调用函数

3.4 本章小结

本章分析了hello.i文件至汇编指令文件hello.s的编译过程,该过程实现了C语言代码向低级呆板语言指令的转化,实现了数据、赋值、各种运算、条件判定与函数使用的代码转化,为不同高级语言的不同编译器提供了通用的输出语言,也为下一步盘算机更好的处理该步伐打下底子。


第4章 汇编

4.1 汇编的概念与作用

汇编是指把汇编语言翻译成二进制呆板语言的过程。
诞生过程:对于人类来说,二进制步伐是不可读的,无法明白呆板干了什么。为了办理可读性的问题,以及偶然的编辑需求,就诞生了汇编语言,为了将汇编语言转化为二进制呆板指令,就必要增长汇编这一步骤。[5]
4.2 在Ubuntu下汇编的下令

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

图4-1 汇编

4.3 可重定位目标elf格式

ELF 头(ELF header)以一个 16 字节的序列开始,这个序列描述了生成该文件的体系的字的大小和字节序次。ELF 头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。此中包括 ELF 头的大小、目标文件的类型(如可重定位、可执行或者共享的)、呆板类型(如 X86-64)、节头部表(section header table)的文件偏移,以及节头部表中条目标大小和数量,如图4-2;
ELF头部数据布局如下:
ELF头部数据布局

typedef struct
{
  unsigned char  e_ident[EI_NIDENT];      /* Magic number and other info */
  Elf64_Half      e_type;                /* Object file type */
  Elf64_Half      e_machine;         /* Architecture */
  Elf64_Word    e_version;           /* Object file version */
  Elf64_Addr     e_entry;               /* Entry point virtual address */
  Elf64_Off e_phoff;                     /* Program header table file offset */
  Elf64_Off e_shoff;                      /* Section header table file offset */
  Elf64_Word    e_flags;               /* Processor-specific flags */
  Elf64_Half      e_ehsize;             /* ELF header size in bytes */
  Elf64_Half      e_phentsize;        /* Program header table entry size */
  Elf64_Half      e_phnum;            /* Program header table entry count */
  Elf64_Half      e_shentsize;        /* Section header table entry size */
  Elf64_Half      e_shnum;            /* Section header table entry count */
  Elf64_Half      e_shstrndx;          /* Section header string table index */
} Elf64_Ehdr;


图4-2 ELF头

不同节的位置和大小是由节头部表描述的,此中目标文件中每个节都有一个固定大小的条目(entry),hello步伐共13个节,如图4-3。

图4-3 节头部表

.symtab:一个符号表,它存放在步伐中定义和引用的函数和全局变量的信息,和编译器中的符号表不同,.symtab 符号表不包含局部变量的条目,观察到此中的函数有main、puts、exit、printf等,如图4-4。

图4-4 .symtab符号表

.rel.text:一个 .text 节中位置的列表,当链接器把这个目标文件和其他文件组合时,必要修改这些位置。一样平常而言,任何调用外部函数或者引用全局变量的指令都必要修改。另一方面,调用当地函数的指令则不必要修改。[1]
当汇编器生成一个目标模块时,它并不知道数据和代码终极将放在内存中的什么位置。它也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。以是,无论何时汇编器遇到对终极位置未知的目标引用,它就会生成一个重定位条目,告诉链接器在将目标文件合并成可执行文件时怎样修改这个引用。代码的重定位条目放在 .rel.text 中,如图4-5。

图4-5 .rel.text重定位节

4.4 Hello.o的结果解析

objdump -d -r hello.o  分析hello.o的反汇编,并请与第3章的 hello.s举行对照分析。
分析呆板语言的构成,与汇编语言的映射关系。特别是呆板语言中的使用数与汇编语言不一致,特别是分支转移函数调用等。
运行下令:objdump -d -r hello.o > hello_obj.s生成hello_obj.s反汇编文件。
与第3章的 hello.s举行对照分析:

  • 在hello.s文件中,跳转指令目标所在使用.L2或.L3情势标示,如图4-6a,而在hello_obj.s中,跳转指令使用相对所在寻址方式标示,如图4-6b;

 

图4-6a hello.s跳转指令


图4-6b hello_obj.s跳转指令


  • 关于调用函数:hello.s中call紧跟被调函数的函数名,而在hello_obj.s中call调用的是当前指令的下一条指令,如图4-7。这是由于被调用的函数为共享库中的函数,必要通过动态链接器才气确定精确的函数所在,在汇编成为呆板语言时,对于这些不确定所在的函数调用,将其call指令后的相对所在设置为全0(目标所在为下一条指令),并在.rela.text 节中为其添加重定位条目,等待静态链接的进一步确定。

图4-7 hello_obj.s调用函数


  • 常量表示的情势不同:hello.s中,常量以十进制情势表示,而在hello_obj.s中,常量均以十六进制情势表示。
4.5 本章小结

本章讨论了hello步伐在汇编过程中发生的变化,分析了其ELF表信息,比较了在编译后的代码与汇编后再反汇编的代码之间的区别。此时,盘算机已经完成了步伐运行过程中的绝大部分任务,但此时该步伐还处于孤立无援的状态,必要链接器来辅助实现真正完备的步伐。

第5章 链接

5.1 链接的概念与作用

链接(linking)是将各种代码和数据片断收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。在现代体系中,链接是由叫做链接器(linker)的步伐自动执行的。[1]
5.2 在Ubuntu下链接的下令

链接下令:ld -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/9/crtbegin.o hello.o -lc /usr/lib/gcc/x86_64-linux-gnu/9/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -z relro -o hello.out

图5-1 链接

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

当hello步伐链接后,生成可执行目标文件。可执行目标文件的格式类似于可重定位目标文件的格式。ELF 头描述文件的总体格式。它还包括步伐的入口点(entry point),也就是当步伐运行时要执行的第一条指令的所在,如图5-2,还可以观察到节头数由14变为30。.text、.rodata 和 .data 节与可重定位目标文件中的节是相似的,除了这些节已经被重定位到它们终极的运行时内存所在以外。.init 节定义了一个小函数,叫做 _init,步伐的初始化代码会调用它。因为可执行文件是完全链接的(已被重定位),以是它不再必要 .rel 节。

图5-2 ELF头

节头部表包含各段的根本信息,包括各段的起始所在,大小等信息,如图5-3。

图5-3 节头部表

5.4 hello的捏造所在空间

使用下令:edb –run hello.out
观察到步伐从0x401000开始,如图5-4,与图5.3中.init偏移量相同;

图5-4 edb加载hello.out

根据图5.3,可以找到各段信息。例如:.init_array位于0x403e00,.fini_array位于0x403e08,./dynamic位于0x403e10,如图5-5;

图5-5 edb查看段信息

5.5 链接的重定位过程分析

objdump -d -r hello 分析hello与hello.o的不同,分析链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
运行下令:objdump -d -r hello.out > hello_obj2.s生成hello_obj2.s文件
分析hello_obj.s与hello_obj2.s的不同:

  • 新增长节:再hello_obj.s中只有.test节,而在hello_obj2.s中新增长了.init、.plt、.plt.sec、.fini四个节;
  • 函数有了详细所在:汇编指令不再从0开始,而是从0x401000开始。在hello_obj.s中调用的函数只有函数名,没有所在,call只能够指向下一条代码的所在;而在hello_objs中,每一个调用的函数都有在.plt.sec节中详细的所在,如图5-6;

图5-6 函数详细所在

链接:链接器根据汇编器在重定位文件中提供的重定位条目就知道它的输入目标模块中的代码节和数据节简直切大小。如今就可以开始重定位步骤了,在这个步骤中,将合并输入模块,并为每个符号分配运行时所在。
重定位分为两步:重定位节和符号定义、重定位节中的符号引用。
第一步未来自所有重定位文件的相同类型节合并,使步伐中的每条指令和全局变量拥有唯一的运行时内存所在;第二步,链接器修改代码节和数据节中对每个符号的引用,使得它们指向精确的运行时所在。
重定位符号引用举例:在hello_obj2.s中,exit函数的所在为0x4010d0,如图5-7,对exit函数的调用如图5-8,callq的二进制下令为e8,此处相对所在偏移量为ff ff fe cb,即-0x00 01 35,相对所在为0x401205,则exit的所在为0x401205 – 0x0135得0x4010d0。

图5-7 exit函数所在


图5-8 调用exit函数

5.6 hello的执行流程

调用的函数及其所在如下:
0x0401000 <_init>
0x0401020 <.plt>
0x0401090 <puts@plt>
0x04010a0 <printf@plt>
0x04010b0 <getchar@plt>
0x04010c0 <atoi@plt>
0x04010d0 <exit@plt>
0x04010e0 <sleep@plt>
0x04010f0 <_start>
0x0401120 <_dl_relocate_static_pie>
0x0401130 <deregister_tm_clones>
0x0401160 <register_tm_clones>
0x04011a0 <__do_global_dtors_aux>
0x04011d0 <frame_dummy>
0x04011d6 <main>
0x0401280 <__libc_csu_init>
0x04012f0 <__libc_csu_fini>
0x04012f8 <_fini>
5.7 Hello的动态链接分析

分析hello步伐的动态链接项目,通过edb/gdb调试,分析在动态链接前后,这些项目标内容变化。要截图标识分析。
动态链接是将共享库加载到恣意的内存所在,并和一个在内存中的步伐链接起来的过程,它是由一个叫做动态链接器(dynamic linker)的步伐来执行的。链接过程中必要用到全局偏移表GOT与过程链接表PLT。在加载时,动态链接器会调用PLT中的函数,重定位GOT中的每个条目,使它包含精确的绝对所在。使用edb分析动态链接过程:
起首,在ELF文件中寻找.got与.got.plt的起始位置,分别为0x403ff0与0x404000,在运行init前,如图5-9,运行之后如图5-10,可以观察到.got变化。

图5-9 运行init前.got条目


图5-10 运行init后.got条目

5.8 本章小结

本章重要讨论了hello.o由重定位目标文件生成可执行文件的过程,分析了在此链接过程中hello.out的ELF表信息、捏造所在空间、重定位与动态链接的过程,较完备的展示了其在链接中发生的变化。此时,我们已经见证了一个步伐完备运行的过程,然而,对于步伐之外的事变我们还没有讨论,步伐与进程的关系,存储空间的分配,以及IO的管理。hello走完了本身的路,开始回忆沿途的风景。

第6章 hello进程管理

6.1 进程的概念与作用

概念:进程就是一个执行中步伐的实例。体系中的每个步伐都运行在某个进程的上下文(context)中。上下文是由步伐精确运行所需的状态组成的。这个状态包括存放在内存中的步伐的代码和数据,它的栈、通用目标寄存器的内容、步伐计数器、环境变量以及打开文件描述符的集合。
作用:进程提供一种假象,就好像我们的步伐是体系中当前运行的唯一的步伐一样。我们的步伐好像是独占地使用处理器和内存。处理器就好像是无间断地一条接一条地执行我们步伐中的指令。最后,我们步伐中的代码和数据好像是体系内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程

shell是一个交互型应用级步伐,代表用户运行其他步伐。如Windows下的下令行解释器,cmd、powershell,图形界面的资源管理器。Linux下的Terminal/tcsh、bash等等,当然也包括图形化的GNOME桌面环境。
处理流程:

  • shell等待用户输入并读取用户输入的下令;
  • 判定是否为shell内置下令,是则运行该下令,否则以为是一个可执行文件;
  • shell构建参数和环境变量;
  • shell通过fork创建子进程,再通过execve函数加载可执行文件。
  • 回收创建的子进程,回到第一步;
6.3 Hello的fork进程创建过程

起首通过fork()函数创建一个shell步伐的子进程,该子进程得到与父进程用户级捏造所在空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。父进程和新创建的子进程之间最大的区别在于它们有不同的 PID。子进程运行时父进程可以等待其运行,也可以将其转到后台,并等待用户下一条指令。子进程运行完毕后会向父进程发送SIGCHLD信号,父进程接收信号后回收子进程。
6.4 Hello的execve过程

execve 函数加载并运行可执行目标文件hello.out,且带参数列表 argv 和环境变量列表 envp。在加载hello.out之后,它调用启动代码设置栈,在当前进程的上下文中加载并运行一个新的步伐,它会覆盖当前进程的所在空间。之后将控制传递给新步伐的主函数main,该主函数有如下情势的原型:
int main(int argc, char **argv, char **envp);
主函数按照代码序次依次执行,最后竣事。
6.5 Hello的进程执行

结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
如图6-1,假设有2进程A和B,A初始运行在用户模式下,直到通过执行体系调用read陷入到内核,内核调用磁盘读取步伐,此过程必要很长一段时间。为了提高运行效率,内核开始执行进程B。则进程A运行的那段时间为进程A的时间片。内核切换进程AB时,必要生存进程A的上下文信息,并开始运行进程B的上下文信息,此过程在内核模式中运行,切换至进程B后,重新在用户模式运行。

图6-1 进程上下文切换的分析

进程B运行过程中,当磁盘读取至内存后,磁盘发送一个制止信号,内核接收信号,并计划重新运行进程A。此过程与之前切换过程根本相同,由用户模式转换至内核模式,并完成进程B的上下文生存与进程A的上下文切换,再换回用户模式,将控制交回进程A中紧随read之后的指令,继续运行进程A。
6.6 hello的非常与信号处理

hello执行过程中会出现哪几类非常,会产生哪些信号,又怎么处理的。
步伐运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps  jobs  pstree  fg  kill 等下令,请分别给出各下令及运行结截屏,分析非常与信号的处理。
会出现的非常有:陷阱、制止;
会出现的信号:ctrl+z发送SIGSTP使进程停息,如图6-3、ctrl+c发送SIGINT使进程制止,如图6-4,SIGCONT由fg发送,SIGKILL由kill发送;
不停乱按会改变输出的光标位置,回车同理,而且乱按的字母会再进程运行完后被shell当作下令或可执行步伐;
输入ps,会表现当前全部进程,包括hello.out、shell与ps;

图6-5 输入ps

输入jobs也会表现进程,但只表现hello.out;
输入pstree,可以表现进程树;

图6-6a 输入pstree


图6-6b 输入pstree


图6-6c 输入pstree

输入ctrl+z再输入fg + 进程号,可以发送信号SIGCONT使制止的进程回到前台继续运行;输入ctrl+z再输入kill,可以向指定进程发送某信号,如此处发送9号SIGKILL信号,杀死该步伐;

6.7本章小结

本章讨论了hello步伐运行时的进程管理,分析分析了shell创建子进程的过程、执行步伐调用execve()函数的过程,还有步伐运行过程中内核的动作与触发的非常和信号,展示了进程从开始运行步伐被父进程创建到步伐竣事被回收的过程。

第7章 hello的存储管理

7.1 hello的存储器所在空间

逻辑所在:包含在呆板语言指令中用来指定一个使用数或一条指令的所在。由一个段标识符加上一个指定段内相对所在的偏移量组成,表示为 [段标识符:段内偏移量]。
线性所在(捏造所在):n位所在空间对应2n个所在,可以远大于物理所在。
物理所在:用于内存芯片级内存单元寻址。
转化关系为:
逻辑所在 -> [分段单元] ->线性(捏造)所在 -> [分页单元] -> 物理所在
由于Linux没有段式管理,以是逻辑=线性=捏造所在。以是hello步伐中初始运行所在0x401000为捏造所在,必要经过盘算与变换才气得到现实物理所在。
7.2 Intel逻辑所在到线性所在的变换-段式管理

一个逻辑所在由两部份组成:段标识符:段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。此中前13位是一个索引号。后面3位包含一些硬件细节,如图7-1;

图7-1 段标识符

通过段标识符中的索引号从GDT或者LDT找到该段的段描述符,段描述符中的base字段是段的起始位置的线性所在。一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程本身的,就放在所谓的“局部段描述符表(LDT)”中。GDT在内存中的所在和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中[7]。终极盘算:
段起始所在+ 段内偏移量 = 线性所在
起首,给定一个完备的逻辑所在[段选择符:段内偏移所在]:
1、看段选择符的T1=0照旧1,知道当前要转换的是GDT中的段,照旧LDT中的段,再根据相应寄存器,得到其所在和大小。我们就拥有了一个数组。
2、由段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它的Base,即基所在就知道了。
3、Base + offset = 线性所在。
7.3 Hello的线性所在到物理所在的变换-页式管理

情势上来说,由捏造所在到物理所在的翻译是一个 N 元素的捏造所在空间(VAS)中的元素和一个 M 元素的物理所在空间(PAS)中元素之间的映射。
图 7-2 展示了 MMU 怎样使用页表来实现这种映射。CPU 中的一个控制寄存器,页表基址寄存器(Page Table Base Register,PTBR)指向当前页表。n 位的捏造所在包含两个部分:一个 p 位的捏造页面偏移(Virtual Page Offset,VPO)和一个(n−p)位的捏造页号(Virtual Page Number,VPN)。MMU 使用 VPN 来选择适当的 PTE。例如,VPN 0 选择 PTE 0,VPN 1 选择 PTE 1,以此类推。将页表条目中物理页号(Physical Page Number,PPN)和捏造所在中的 VPO 串联起来,就得到相应的物理所在。注意,因为物理和捏造页面都是 P 字节的,以是物理页面偏移(Physical Page Offset,PPO)和 VPO 是相同的。[1]

图 9-12 使用页表的所在翻译

如图7-3,此处只假设页面命中环境,CPU 硬件执行以下步骤:
第 1 步:处理器生成一个捏造所在,并把它传送给 MMU。
第 2 步:MMU 生成 PTE 所在,并从高速缓存/主存哀求得到它。
第 3 步:高速缓存/主存向 MMU 返回 PTE。
第 4 步:MMU 构造物理所在,并把它传送给高速缓存/主存。
第 5 步:高速缓存/主存返回所哀求的数据字给处理器。

图7-3 页面命中

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

图 7-4 给出了 Core i7 MMU怎样使用四级的页表来将捏造所在翻译成物理所在。36 位 VPN 被分别成四个 9 位的片,每个片被用作到一个页表的偏移量。CR3 寄存器包含 L1 页表的物理所在。VPN1提供到一个 L1 PTE 的偏移量,这个 PTE 包含 L2 页表的基所在。VPN2提供到一个 L2 PTE 的偏移量,以此类推。

图7-4 Corei7所在翻译[1]

(PT:页表,PTE:页表条目,VPN:捏造页号,VPO:捏造页偏移,PPN:物理页号,PPO:物理页偏移量。图中还给出了这四级页表的 Linux 名字)

TLB 是一个小的、捏造寻址的缓存,此中每一行都生存着一个由单个 PTE 组成的块。TLB 通常有高度的相联度。如图 9-15 所示,用于组选择和行匹配的索引和标记字段是从捏造所在中的捏造页号中提取出来的。如果 TLB 有T=2t个组,那么 TLB 索引(TLBI)是由 VPN 的 t 个最低位组成的,而 TLB 标记(TLBT)是由 VPN 中剩余的位组成的,如图7-5。

图 7-5 捏造所在中用以访问 TLB 的组成部分

图 7-6a 展示了当 TLB 命中时(通常环境)所包括的步骤。这里的关键点是,所有的所在翻译步骤都是在芯片上的 MMU 中执行的,因此非常快。
第 1 步: CPU 产生一个捏造所在。
第 2 步和第 3 步: MMU 从 TLB 中取出相应的 PTE。
第 4 步: MMU 将这个捏造所在翻译成一个物理所在,而且将它发送到高速缓存/主存。
第 5 步:高速缓存/主存将所哀求的数据字返回给 CPU。

图 7-6 TLB 命中和不命中的使用图

当 TLB 不命中时,MMU 必须从 L1 缓存中取出相应的 PTE,如图 9-16b 所示。新取出的 PTE 存放在 TLB 中,可能会覆盖一个已经存在的条目。[1]
图 7-7 总结了完备的 Core i7 所在翻译过程,从 CPU 产生捏造所在的时刻一直到来自内存的数据字到达 CPU。Core i7 采用四级页表条理布局。每个进程有它本身私有的页表条理布局。当一个 Linux 进程在运行时,虽然 Core i7 体系布局允许页表换进换出,但是与已分配了的页相关联的页表都是驻留在内存中的。CR3 控制寄存器指向第一级页表(L1)的起始位置。CR3 的值是每个进程上下文的一部分,每次上下文切换时,CR3 的值都会被恢复。

图 7-7 Core i7 所在翻译的概况。为了简化,没有表现 i-cache、i-TLB 和 L2 同一 TLB

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

如图7-7,根据7.4节的讨论,当查找到52位物理所在PA后,可以盘算出L1每个块64B=26,以是块偏移CO为6位;由于64组,以是CI为6位,剩下为标记位。
根据CI查找组,CT查找缓存标记,CO查看有效位,有效位为1代表命中;否则不命中,必要查找下一级缓存L2,以此类推。
7.6 hello进程fork时的内存映射

当 fork 函数被当前进程调用时,内核为新进程创建各种数据布局,并分配给它一个唯一的 PID。为了给这个新进程创建捏造内存,它创建了当前进程的 mm_struct、区域布局和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域布局都标记为私有的写时复制。
当 fork 在新进程中返回时,新进程如今的捏造内存刚好和调用 fork 时存在的捏造内存相同。当这两个进程中的任一个厥后举行写使用时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有所在空间的抽象概念。[1]
7.7 hello进程execve时的内存映射

假设在当前进程中的步伐执行了如下的 execve 调用:
execve("hello.out", NULL, NULL);
execve 函数在当前进程中加载并运行包含在可执行目标文件 hello.out 中的步伐,用 hello.out 步伐有效地替换了当前步伐。加载并运行 hello.out 必要以下几个步骤:

  • 删除已存在的用户区域。删除当前进程捏造所在的用户部分中的已存在的区域布局。
  • 映射私有区域。为新步伐的代码、数据、bss 和栈区域创建新的区域布局。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为 hello.out 文件中的. text 和. data 区。bss 区域是哀求二进制零的,映射到匿名文件,其大小包含在 hello.out 中。栈和堆区域也是哀求二进制零的,初始长度为零。图 7-8 概括了私有区域的不同映射。
  • 映射共享区域。如果 hello.out 步伐与共享对象(或目标)链接,比如标准 C 库 libc.so,那么这些对象都是动态链接到这个步伐的,然后再映射到用户捏造所在空间中的共享区域内。
  • 设置步伐计数器(PC)。execve 做的最后一件事变就是设置当前进程上下文中的步伐计数器,使之指向代码区域的入口点。
下一次调度这个进程时,它将从这个入口点开始执行。Linux 将根据必要换入代码和数据页面。

图 7-8 加载器是怎样映射用户所在空间的区域的

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

DRAM 缓存不命中称为缺页(page fault),其会触发一个缺页非常。缺页非常调用内核中的缺页非常处理步伐,该步伐会选择一个捐躯页,如果该捐躯页已经被修改了,那么内核就会将它复制回磁盘。接下来,内核从磁盘复制将调用的页到内存中捐躯页的位置,更新页表条目,随后返回。
当非常处理步伐返回时,它会重新启动导致缺页的指令,该指令会把导致缺页的捏造所在重发送到所在翻译硬件。此时,将调用的页已经在主存中了,那么页命中也能由所在翻译硬件正常处理了。
7.9动态存储分配管理

动态内存分配器维护着一个进程的捏造内存区域,称为堆(heap)。体系之间细节不同,但是不失通用性,假设堆是一个哀求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的所在)。对于每个进程,内核维护着一个变量 brk(读做 “break”),它指向堆的顶部。
分配器将堆视为一组不同大小的块(block)的集合来维护。每个块就是一个连续的捏造内存片(chunk),要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用步伐使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被开释,这种开释要么是应用步伐显式执行的,要么是内存分配器自身隐式执行的。
分配器有两种根本风格。两种风格都要求应用显式地分配块。它们的不同之处在于由哪个实体来负责开释已分配的块。

  • 显式分配器(explicit allocator),要求应用显式地开释任何已分配的块。例如:C标准库提供的malloc步伐包中的分配器;
  • 隐式分配器(implicit allocator),另一方面,要求分配器检测一个已分配块何时不再被步伐所使用,那么就开释这个块。隐式分配器也叫做垃圾收集器(garbage collector),而自动开释未使用的已分配的块的过程叫做垃圾收集(garbage collection)。例如:Java中的垃圾收集。
显式分配器的两种重要数据布局:
隐式空闲链表:空闲块是通过头部中的大小字段隐含地连接着的。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。其长处是简单。明显的缺点是任何使用的开销,例如放置分配的块,要求对空闲链表举行搜刮,该搜刮所需时间与堆中已分配块和空闲块的总数呈线性关系。
显式空闲链表:将堆组织成一个双向空闲链表,在每个空闲块中,都包含一个 pred(前驱)和 succ(后继)指针。可以使用后进先出或按照所在序次来维护链表。其长处是使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间;缺点是空闲块必须充足大,以包含所有必要的指针,以及头部和可能的脚部。
7.10本章小结

本章讨论了盘算机对hello步伐的存储管理与分配机制,分析了其不同所在之间的变换过程,TLB、多级页表与Cache的寻址与访问,fork与execve函数的内存映射过程,缺页处理,动态所在的分配管理。见证了hello从诞生在内存中,又终极离去的全部过程。

第8章 hello的IO管理

8.1 Linux的IO装备管理方法

装备的模子化:文件。一个 Linux 文件就是一个 m 个字节的序列:B0,B1,B2…,Bm,所有的 I/O 装备(例如网络、磁盘和终端)都被模子化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。
装备管理:unix io接口。这种将装备优雅地映射为文件的方式,允许 Linux 内核引出一个简单、低级的应用接口,称为 Unix I/O,这使得所有的输入和输出都能以一种同一且一致的方式来执行。
8.2 简述Unix IO接口及其函数

8.2.1打开与关闭文件
打开文件。一个应用步伐通过要求内核打开相应的文件,来宣告它想要访问一个 I/O 装备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有使用中标识这个文件。内核记录有关这个打开文件的所有信息。应用步伐只需记住这个描述符。
进程是通过调用 open 函数来打开一个已存在的文件或者创建一个新文件的:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(char *filename, int flags, mode_t mode);
// 返回:若成功则为新文件描述符,若堕落为 -1。
关闭文件。当应用完成了对文件的访问之后,它就关照内核关闭这个文件。作为相应,内核开释文件打开时创建的数据布局,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种缘故原由制止时,内核都会关闭所有打开的文件并开释它们的内存资源。
进程通过调用 close 函数关闭一个打开的文件。
#include <unistd.h>
int close(int fd);
// 返回:若成功则为 0,若堕落则为 -1。
8.2.2读写文件
一个读使用就是从文件复制 n > 0 个字节到内存,从当前文件位置 k 开始,然后将 k 增长到 k+n。给定一个大小为 m 字节的文件,当k ⩾ m时执行读使用会触发一个称为 end-of-file(EOF)的条件,应用步伐能检测到这个条件。在文件结尾处并没有明确的 “EOF 符号”。
应用步伐是通太过别调用 read 和 write 函数来执行输入和输出的。
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t n);
// 返回:若成功则为读的字节数,若 EOF 则为0,若堕落为 -1。
ssize_t write(int fd, const void *buf, size_t n);
// 返回:若成功则为写的字节数,若堕落则为 -1。
read 函数从描述符为 fd 的当前文件位置复制最多 n 个字节到内存位置 buf。返回值 -1 表示一个错误,而返回值 0 表示 EOF。否则,返回值表示的是现实传送的字节数量。write 函数从内存位置 buf 复制至多 n 个字节到描述符 fd 的当前文件位置。
8.2.3改变当前的文件位置。
对于每个打开的文件,内核保持着一个文件位置 k,初始为 0。这个文件位置是从文件开头起始的字节偏移量。应用步伐能够通过执行 seek 使用,显式地设置文件的当前位置为 k。
8.3 printf的实现分析

从vsprintf生成表现信息,到write体系函数,到陷阱-体系调用 int 0x80或syscall等.
起首观察printf的函数体:
int printf(const char *fmt, ...)
{
int i;
char buf[256];
   va_list arg = (va_list)((char*)(&fmt) + 4);
    i = vsprintf(buf, fmt, arg);
   write(buf, i);
   return i;
 }
此中,“...” 是可变形参的一种写法;va_list的定义:typedef char *va_list。
(char*)(&fmt) + 4) 表示的是...中的第一个参数。
接下来可以再看vsprintf(buf, fmt, arg),如图8-1:

图8-1 vsprintf函数

该函数的功能是格式化。它担当确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数举行格式化,产生格式化输出。
再来追踪write函数:
    write:
     mov eax, _NR_write
     mov ebx, [esp + 4]
     mov ecx, [esp + 8]
     int INT_VECTOR_SYS_CALL
此处为几个寄存器传递了几个参数,然后以int竣事。
INT_VECTOR_SYS_CALL的实现:
init_idt_desc(INT_VECTOR_SYS_CALL, DA_386IGate, sys_call, PRIVILEGE_USER);即通过体系来调用sys_call这个函数。
观察sys_call的实现:

图8-2 sys_call函数

此中call save,是为了生存制止前进程的状态。此处实现很复杂,若只是明白printf的实现的话,可以这样实现sys_call:

图8-3 sys_call简洁实现

字符表现驱动子步伐:从ASCII到字模库到表现vram(存储每一个点的RGB颜色信息)。
表现芯片按照革新频率逐行读取vram,并通过信号线向液晶表现器传输每一个点(RGB分量)。
8.4 getchar的实现分析

getchar可用宏实现:#define getchar() getc(stdin);它从标准输入里读取下一个字符,返回类型为int型,为用户输入的ASCII码或EOF。
当getchar被调用时,步伐等待用户输入。输入字符被存放于键盘缓冲区中。直到输入回车为止(回车字符也放在缓冲区中)。键入回车之后,getchar开始从stdin流中每次读入一个字符。getchar函数的返回值是用户输入的字符的ASCII码,若文件结尾(End-Of-File)则返回-1(EOF),并将用户输入的字符回显到屏幕。
异步非常-键盘制止的处理:键盘制止处理子步伐。担当按键扫描码转成ascii码,生存到体系的键盘缓冲区。
getchar等调用read体系函数,通过体系调用读取按键ascii码,直到担当到回车键才返回。
8.5本章小结

本章分析了hello步伐的IO管理过程,介绍了Linux的IO装备管理方法,根本的Unix IO函数,分析了printf()与getchar()函数的体系级实现过程。从本章的分析可以还观察到步伐输入输出与内核、硬件的巧妙配合。


结论

从被输入编辑器中存为hello.c,经过预处理生成hello.i,被编译生成汇编语言文件hello.s,再通过汇编生成二进制可重定位目标文件hello.o,最后链接器将多个可重定位文件(.o)组合生成终极的可执行文件hello.out,hello经历了本身的一生。
它这一生并不孤独,虽然一开始被菜鸟无情抛弃,但却遇到好心的IO与shell,从键盘输入下令,调用fork()与execve()函数为hello生成进程、加载执行代码文件;还有热心的os(内核),从捏造所在转换到体系非常与制止处理,一路为其保驾护航;TLB、多级页表与Cache三兄弟联合为步伐内存分配推波助澜,使其顺利抵达步伐圣地——CPU;当它拿着运行结果再次找到IO管理时,已经历无数坎坷,此时再次遇到当年谁人菜鸟,他也屡屡遭遇风雨,看着一行行输出,终于领悟到hello的真谛。
从菜鸟到如今略有所得,再次回首hello步伐,发现此中的种种细节均不是只用代码就可以办理的,内存分配,非常处理,以及从初始代码到终极可执行步伐的过程,都蕴藏着庞大的细节,必要各个环节的配合与协调;别的,还有进程、捏造所在、动态链接等概念的提出,它们为步伐打开了新的大门,使得步伐间彼此独立又相互关联,实现了更高级的抽象,这也是盘算机真正的迷人之处。
写在公共平台:由于平台限制未能在格式上做到赏心悦目,且本文仅为提交课程大作业必要,水平仍有不足,望指正!

附件

文件名

作用

hello.c

初始C代码文件

hello.i

预处理后文件

hello.s

编译后汇编语言文件

hello.o

汇编后二进制文件

hello_obj.s

hello.o反汇编文件

hello.out

链接后可执行步伐

hello_obj2.s

hello.out反汇编文件


参考文献


  • Randal E.Bryant, David O'Hallaron. 深入明白盘算机体系[M]. 机械工业出版社.2016.11
  • Pianistx.printf 函数实现的深入分析[EB/OL]. 2013[2021-6-9]. https://www.cnblogs.com/pianist/p/3315801.html.
  • ^_^ 小小码nong.C语言中怎样去明白预处理阶段[EB/OL]. 2017. https://blog.csdn.net/qq_29924041/article/details/54917521
  • C语言编译和链接详解 [EB/OL]. https://c.biancheng.net/view/1736.html
  • 阮一峰. 汇编语言入门教程[EB/OL]. 2018. https://www.ruanyifeng.com/blog/2018/01/assembly-language-primer.html
  • 不会写代码的丝丽. ELF格式解读-(1) elf头部与节头[EB/OL]. 2022. https://blog.csdn.net/qfanmingyiq/article/details/124295287#t13
  • 闫晟. 逻辑所在、物理所在、捏造所在[EB/OL]. 2020. https://blog.csdn.net/TYUTyansheng/article/details/108148566
  • hello步伐运行过程综述(IA-32版本)[EB/OL]. https://ysyx.oscc.cc/slides/hello-x86.html

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

本帖子中包含更多资源

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

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

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

本版积分规则

用多少眼泪才能让你相信

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表