HIT-ICS大作业报告

 

计算机系统

大作业

题     目  程序人生-Hello’s P2P 

专       业   计算机与科学技术                    

学     号   120L021511                    

班     级    2003002                  

指 导 教 师     史先俊                  

计算机科学与技术学院

2022年5月

摘  要

本文在linux环境下,借助GDB,EDB,objdump,readelf等工具手段一步步实现hello.c程序的预处理,编译,汇编,链接。通过在shell里运行分析hello对加载,运行过程。本文依据计算机系统的知识对上述每一步骤都进行了详细的讲解。

关键词:linux;预处理;编译;汇编;链接;进程;存储;IO                           

目  录

第1章 概述................................................................................................................ - 4 -

1.1 Hello简介......................................................................................................... - 4 -

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

1.3 中间结果............................................................................................................ - 4 -

1.4 本章小结............................................................................................................ - 5 -

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

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

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

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

2.4 本章小结............................................................................................................ - 6 -

第3章 编译................................................................................................................ - 9 -

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

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

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

3.4 本章小结.......................................................................................................... - 11 -

第4章 汇编.............................................................................................................. - 13 -

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

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

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

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

4.5 本章小结.......................................................................................................... - 17 -

第5章 链接.............................................................................................................. - 18 -

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

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

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

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

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

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

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

5.8 本章小结.......................................................................................................... - 26 -

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

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

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

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

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

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

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

6.7本章小结.......................................................................................................... - 29 -

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

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

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

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

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

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

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

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

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

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

7.10本章小结........................................................................................................ - 36 -

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

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

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

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

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

8.5本章小结.......................................................................................................... - 41 -

结论............................................................................................................................ - 42 -

附件............................................................................................................................ - 43 -

参考文献.................................................................................................................... - 44 -

第1章 概述

1.1 Hello简介

Hello的自白

P2P: From Program to Process

懵懵懂懂的你笨笨磕磕的将我一字一键敲进电脑存成hello.c(Program,无意识中将我预处理、编译、汇编、链接,历经艰辛-神秘-高贵-欣喜,我-Hello一个完美的生命诞生了。

在壳(Bash)里,伟大的OS(进程管理)为我fork(Process,为我execve,为我mmap,分我时间片,让我得以Hardware(CPU/RAM/IO)上驰骋(取指译码执行/流水线等);

OS(存储管理)与MMU为VA到PA操碎了心;TLB、4级页表、3级Cache,Pagefile等等各显神通为我加速;IO管理与信号处理使尽了浑身解数,软硬结合,才使我能在键盘、主板、显卡、屏幕间游刃有余, 虽然我在台上的表演只是一瞬间、演技看起来很Low、效果很惨白。

O2O: From Zero-0 to Zero-0

我朝 CS(计算机系统-Editor+Cpp+Compiler+AS+LD + OS + CPU/RAM/IO)挥一挥手,不带走一片云彩!

P2P过程:首先先有个hello.c的c程序文本,经过预处理->编译->汇编->链接四个步骤生成一个hello的二进制可执行文件,然后由shell新建一个进程,在那里执行。

020过程:shell执行hello,为其映射虚拟内存,开始执行hello的程序,然后在运行时发生缺页的时候载入物理内存,将其output显示到屏幕,然后hello进程结束,shell回收子进程。

1.2 环境与工具

1.2.1 硬件环境
2.8GHz Intel Core I5;16GB 2133MHz LPDDR3
1.2.2 软件环境
Ubuntu19.0.0 Vmware Fusion 11.0,macOS High Sierra(10.13.6)

1.2.3 开发工具
Codeblocks,Valgrind; gprof; GDB,edb,hexedit, xcode

1.3

中间结果

hello.i(hello.c预处理之后的程序文本)
hello.s(hello.i编译成汇编语言之后的程序文本)
hello.o(hello.s生成的二进制文件)
hello(可执行的hello二进制文件)

hello.oelf(hello.o的elf文件)

hello.o.obj(hello.o的反汇编文件)

hello.elf(hello的elf文件)

hello.obj(hello的反汇编文件)

1.4 本章小结

本章介绍了hello,硬件环境,软件环境,开发工具和运行hello即调试开发过程中生成的产物。

第2章 预处理

2.1 预处理的概念与作用

2.1.1预处理的概念:

预处理是对于c语言程序进行一个初步整理的过程,处理掉了所有的宏定义删除“#define”并展开所定义的宏,处理所有条件预编译指令,如“#if”,“#ifdef”, “#endif”等插入头文件到“#include”处,可以递归方式进行处理删除所有的注释“//”和“/* */”添加行号和文件名标识,以便编译时编译器产生调试用的行号信息,保留所有#pragma编译指令(编译器需要用)。

2.1.2预处理作用:

预处理可以在在将c程序转化为s的汇编程序之前对于宏定义处理,方便后续的代码转化,并且对于在汇编中无用的注释进行处理,删去无用部分对后续操作做准备。

2.2在Ubuntu下预处理的命令

2.3 Hello的预处理结果解析

首先最直观的就是添加了行号,去除了注释,

其次因为预处理中会展开以#起始的行,试图解释为预处理指令,所以可以看到

在hello.i文件中,对hello.c中的头文件进行解析,主要是将#include <stdio.h>、#include <unistd.h> 、#include <stdlib.h>所解析出的代码加在了hello中。

C预处理器不是编译器的组成部分,但是它是编译过程中一个单独的步骤。简言之,C 预处理器只不过是一个文本替换工具而已,它们会指示编译器在实际编译之前完成所需的预处理。

所有的预处理器命令都是以井号(#)开头。它必须是第一个非空字符,为了增强可读性,预处理器指令应从第一列开始。下面列出了所有重要的预处理器指令:

2.4 本章小结

本章节简单介绍了c语言在编译前的预处理过程,简单介绍了预处理过程的概念和作用,对预处理过程进行演示。可以通过实验发现预处理是执行程序前非常重要的一步

第3章 编译

3.1 编译的概念与作用

编译概念:

编译器将文本文件 hello.i 翻译成文本文件 hello.s,它包含一个汇编语言程序。其以高级程序设计语言书写的源程序作为输入,而以汇编语言或机器语言表示的目标程序作为输出。

编译作用:

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

3.2 在Ubuntu下编译的命令

3.3 Hello的编译结果解析

接下来我们逐个分析其中的数据类型:

3.3.1

数据:常量、变量(全局/局部/静态)、表达式、类型、宏

 全局变量:这里的main就是一个全局变量

       

字符串:

栈与指针:rbp入栈,将rbp的值赋值给rsp,随后将rsp减去32,以此来开辟出4个字节的空间,随后各自给edi,rsi赋值

3.3.2赋值

3.3.3类型转换

3.3.4算术运算ALU

3.3.5移位操作

3.3.6转移:将4与edi中的值进行比较,若相等,则跳转到.L2

3.3.7函数调用

3.4 本章小结

本节主要介绍的是编译器通过编译由.i文件生成汇编语言的.s文件的过程,并通过解析局部变量在.s文件中的表示方法,以及各类c语言的基本语句的汇编表示,让读者更加理解高级语言的底层表示方法。汇编语言与编译过程的学习使我们真正理解计算机底层的一些执行方法。

第4章 汇编

4.1 汇编的概念与作用

汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。

实现将汇编代码转换为机器指令,使之在链接后能够被计算机直接执行。

4.2 在Ubuntu下汇编的命令

4.3 可重定位目标elf格式

  1. ELF头

ELF头以一个16字节的序列Magic开始,该序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助连接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(可重定位/可执行/共享)、机器类型、节头部表的文件偏移,以及节头部表条目的大小和数量。

由hello.o文件的ELF头部分可知:该文件为REL(可重定位目标文件);机器类型为AMD X86-64;节头部表的文件偏移为0;字节顺序为小端序;节头大小为64字节;节头数量为14

  1. 节头部表

Linux系统使用可执行可链接格式(ELF)对目标文件进行组织,其具体结构及其内容如图所示

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

.data节:已初始化的静态和全局C变量。类型为PROGBITS,意为程序数据,旗标为WA,即权限为可分配可写。

.bss节:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。在目标文件中这个节不占据实际的空间,它仅仅是一个占位符。类型为NOBITS,意为暂时没有存储空间,旗标为WA,即权限为可分配可写。

.rodata节:存放只读数据,例如printf中的格式串和开关语句中的跳转表。类型为PROGBITS,意为程序数据,旗标为A,即权限为可分配。

.comment节:包含版本控制信息。 .note.GNU_stack节:用来标记executable stack(可执行堆栈)。

.eh_frame节:处理异常。 .rela.eh_frame节:.eh_frame的重定位信息。

.shstrtab节:该区域包含节区名称。 .symtab节:一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。

.strtab节:一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部的节名字。

  1. 符号表

符号表每个条目都对应一个符号的语义,具体包括:

①符号名称name:以整型变量存放在符号表中,是字符串表.strtab中的字节偏移,指向符号的以null结尾的字符串名字。

②符号地址value:距定义目标的节的起始位置的偏移,对于可执行目标文件来说是一个绝对的运行时地址。

③符号类型type:表明符号的类型,通常要么是数据,要么是函数。

④符号范围binding:该字段表明符号是本地的还是全局的。

⑤分配目标section:该字段是一个到节头部表的索引,表明对应符号被分配至目标文件的某个节;

有三个特殊的伪节,它们在节头部表中是没有条目的:ABS代表不该被重定位的符号;UNDEF代表未定义的符号,即本模块中引用的外部符号;COMMON表示还未被分配位置的未初始化的数据目标。

⑥目标大小size

  1. 重定位节

①offset:需要被修改的引用的字节偏移。
②symbol:标识被修改引用应该指向的符号。
③type:告知链接器如何修改新的引用。
④addend:一些类型的重定位要使用它对被修改引用的值做偏移调整

4.4 Hello.o的结果解析

分析其与hello.s的差异:

  1. 对函数的调用与重定位条目对应

在可重定位文件中call后面不再是函数的具体名称,而是一条重定位条目指引的信息。而在汇编文件中可以看到,call后面直接加的是文件名。

  1. 分支转移

hello.s:代码直接声明具体的段存储位置,操作数为助记符

反汇编代码:计算出地址,依据地址跳转:

     反汇编的跳转指令用的不是段名称比如.L3,而是用的确定的地址。但

反汇编代码中,分支转移表示为主函数+段内偏移量。反汇编代码跳转指令的操作数使用的不是段名称,因为段名称只是在汇编语言中便于编写的助记符,所以在汇编成机器语言之后显然不存在,而是确定的地址。

  1. 立即数变为16进制格式

hello.s:立即数为10进制格式:

反汇编代码:立即数为16进制格式:

在编译文件中,立即数全部是以16进制表示的,因为16进制与2进制之间的转换比十进制更加方便,所以都转换成了16进制。

4.5 本章小结

本章介绍了汇编的整个过程,从汇编语言到机器码,重点关注了生成文件hello.o可重定位这一特性,并且通过objdump反汇编得到的代码与hello.s进行比较,了解了其中的映射机制以及两者之间的区别。

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.ohello.o /usr/lib/x86_64-linux-gnu/libc.so/usr/lib/x86_64-linux-gnu/crtn.o

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

  1. ELF头

ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表(section header table) 的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的条目(entry)。

  1. 节头部表

hello.o的节头部表作对比,可以得出:hello的节头部表增加了若干条目,下图中一共有26个节的信息,而hello.o的节头部表中只有13个节的信息。所有节被分配了运行时的地址。可以看到下图中某些节地址有所不同,而hello.o的节头部表中所有节的Address为全0

  1. 程序头表

  1. 重定位节

  1. 符号表

hello程序的符号表包含编号Num、Value、Size、Type、Bind、Vis、Ndx、Name字段。其含义可以参照4.3.3内容,在此不做赘述。

可以看到,可执行目标文件的符号表表项数目(51 entries)明显多于可重定位目标文件的表项数目(18 entries)。一方面,可执行目标文件中加入了与调试、加载、动态链接相关的节,使得表示节的符号数增多;另一方面,由于链接器对可重定位目标文件中的符号进行了进一步解析,加入了若干系统调用。

  1. 动态符号表

动态符号表 (.dynsym) 用来保存与动态链接相关的导入导出符号,不包括模块内部的符号。而 .symtab 则保存所有符号,包括 .dynsym 中的符号。

5.4 hello的虚拟地址空间

 分析程序头LOAD可加载的程序段的地址为0x400000,调用EDB:

由节头部表知.text节起始地址为0x4010d0, 结束于内存地址0x401215

由节头部表知.rodata节起始地址为0x402000,结束于地址0x40202f

  

5.5 链接的重定位过程分析

objdump -d hello > hello_asm.txt 反汇编hello文件

分析hello.o与hello的区别:

  1. 虚拟地址不同
    hello.o:反汇编代码虚拟地址从0开始

hello:反汇编代码虚拟地址从0x400000开始

  1. 反汇编节数不同
    hello.o:只有.text节,其中只有main函数的反汇编代码

hello:在main函数之前填充有链接过程中重定位而加入进来各种函数、数据,增加了.init.plt.plt.sec等节的反汇编代码。

  1. call函数跳转地址,引用全局变量地址不同, 不过注意到,相对地址没有改变。

Hello重定位地址的计算:

(1)重定位节和符号定义链接器将所有类型相同的节合并在一起后,这个节就作为可执行目标文件的节。然后链接器把运行时的内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号,当这一步完成时,程序中每条指令和全局变量都有唯一运行时的地址。

(2)重定位节中的符号引用这一步中,连接器修改代码节和数据节中对每个符号的引用,使他们指向正确的运行时地址。执行这一步,链接器依赖于可重定位目标模块中称为的重定位条目的数据结构。

(3)重定位条目当编译器遇到对最终位置未知的目标引用时,它就会生成一个重定位条目,代码的重定位条目放在.rel.txt中

5.6 hello的执行流程

ld-2.27.so!_dl_start 0x00007f294d8086c0

ld-2.27.so!_dl_init 0x00007f294d808c50

hello!_start 0x400550

hello!init 0x401000

hello_main 0x401105

hello!puts@plt 0x401080

hello!exit@plt 0x4010b0

hello!printf@plt 0x401090

hello!sleep@plt 0x404044

hello!getchar@plt 0x404028

sleep@plt 0x4010c0

5.7 Hello的动态链接分析

动态链接项目中,查看dl_init前后项目变化。对于动态共享链接库中PIC函数,编译器加重定位记录,等待动态链接器处理,为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略,将过程地址的绑定推迟到第一次调用该过程。动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。

GOT运行时地址为0x403ff0,PLT的运行时地址为0x404000。

在程序调用dl_init前,使用edb查看地址0x404000处的内容,如下所示:

 

dl_init调用之前,对于每一条PIC函数调用,调用的目标地址都实际指向PLT中的代码逻辑,初始时每个GOT条目都指向对应的PLT条目的第二条指令。
调用前:

调用后:

在dl_init调用前后, 0x6008b0和0x6008c0处的两个8字节的数据分别发生改变。和PLT联合使用时,GOT[0]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。其中GOT[1]指向重定位表(依次为.plt节需要重定位的函数的运行时地址)用来确定调用的函数地址, GOT[2]是动态链接器ld-linux.so模块中的入口点。

  在之后的函数调用时,首先跳转到PLT执行.plt中逻辑,第一次访问时,GOT地址为下一条指令,将函数序号压栈,然后跳转到PLT[0],在PLT[0]中将重定位表地址压栈,然后访问动态链接器,在动态链接器中使用函数序号和重定位表确定函数运行时地址,重写GOT,再将控制传递给目标函数。之后如果对同样函数调用,第一次访问跳转直接跳转到目标函数。

5.8 本章小结

本章主要介绍了链接的概念与作用,链接可分为符号定义和重定位,了解了可执行文件的ELF格式,分析了hello的虚拟地址空间,重定位过程,执行过程,动态连接过程,对链接有了更深的理解。

6章 hello进程管理

6.1 进程的概念与作用

概念:进程是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文中。

作用:进程提供给应用程序关键抽象:
①一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器。②一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用内存系统。

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

1.Shell-bash的作用:

Linux系统中,Shell是一个交互型应用级程序,为使用者提供操作界面,接收用户命令,然后调用相应的应用程序。

2.处理流程:

①从终端读入输入的命令。

②将输入字符串切分获得所有的参数。

③检查第一个命令行参数是否是一个内置的shell命令,如果是则立即执行。

④如果不是内部命令,调用fork( )创建新进程/子进程执行指定程序。

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

6.3 Hello的fork进程创建过程

终端程序通过调用fork()函数创建一个子进程,子进程得到与父进程完全相同但是独立的一个副本,包括代码段、数据段、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,父进程和子进程最大的不同时他们的PID是不同的。父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的逻辑控制流的指令。在子进程执行期间,父进程默认选项是显示等待子进程的完成。

我们在shell上输入./hello,由于这不是一个内置的shell命令,所以shell会认为hello是一个可执行目标文件,通过调用某个驻留在存储器中被称为加载器的操作系统代码来运行它。

6.4 Hello的execve过程

当调用fork()函数创建了一个子进程之后,子进程调用exceve函数在当前子进程的上下文加载并运行一个新的程序即hello程序,需要以下步骤:

  • 删除已存在的用户区域。删除之前进程在用户部分中已存在的结构。

②创建新的代码、数据、堆和栈段。所有这些区域结构都是私有的,写时复制的。虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区。bss区域是请求二进制零的,映射匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零。

③映射共享区域。如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域。

④设置程序计数器(PC)。exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。

6.5 Hello的进程执行

hello程序的执行是依赖于进程所提供的抽象的基础上,进程提供给应用程序的抽象有:

1.一个独立的逻辑控制流,它提供一个假象,好像我们的进程独占的使用处理器

2. 一个私有的地址空间,它提供一个假象,好像我们的程序独占的使用CPU内存。

操作系统所提供的进程抽象:

①逻辑控制流:如果想用调试器单步执行程序,我们会看到一系列的程序计数器(PC)的值,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或是包含在运行时动态链接到程序的共享对象中的指令。这个PC值的序列叫做逻辑控制流,或者简称为逻辑流。

②上下文切换:如果系统调用因为等待某个事件发生而阻塞,那么内核可以让当前进程休眠,切换到另一个进程,上下文就是内核重新启动一个被抢占的进程所需要的状态,是一种比较高层次的异常控制流。

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

④用户模式和内核模式:shell使得用户可以有机会修改内核,所以需要设置一些防护措施来保护内核,如限制指令的类型和可以作用的范围。

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

hello进程的执行:在进程调用execve函数之后,进程已经为hello程序分配了新的虚拟的地址空间,最初hello运行在用户模式下,输出hello 1190201016 石衍,然后调用sleep函数进程进入内核模式,运行信号处理程序,之后再返回用户模式。运行过程中,cpu不断切换上下文,使运行过程被切分成时间片,与其他进程交替占用cpu,实现进程的调度。

6.5 Hello的进程执行

上下文切换:操作系统内核使用一种称为上下文切换的较高层形式的异常控制流来实现多任务。上下文切换机制是建立在较低层异常机制之上的。内核为每个进程维持-一个.上下文。上下文就是内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表、包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。

调度:在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行时,我们说内核调度了这个进程。

如果系统调用因为等待某个事件发生而阻塞,那么内核可以让当前进程休眠,切换到另一个进程。比如,如果一个read系统调用需要访问磁盘,内核可以选择执行上下文切换,运行另外一个进程,而不是等待数据从磁盘到达。另一个示例是sleep系统调用,它显式地请求让调用进程休眠。

hello程序与操作系统其他进程通过操作系统的调度,切换上下文,拥有各自的时间片从而实现并发运行。hello在调用sleep函数时做了上下文切换。

hello初始运行在用户模式中,直到它通过执行系统调用sleep陷入到内核。内核处理休眠请求主动释放当前进程(hello),同时计时器开始计时,内核进行如上图所示的上下文切换,将当前进程的控制权交给其他进程,当进程达到sleep_secs的时间时,给其他进程发送中断信号,触发中断异常处理子程序,将hello进程从等待队列中移出,重新加入到运行队列。

6.6 hello的异常与信号处理

hello执行过程中进行如下操作:
(1)键盘随机按键:如果按键过程中没有回车键,会把输入屏幕的字符串缓存起来;如果按键过程中有回车键,则当程序运行完成后,缓存区中的换行符前的字符串会被shell当作指令执行。

(2)按Ctrl-Z键
输入Ctrl-Z键会发送一个SIGTSTP信号给前台进程组的每一个进程,故hello进程停止。运行ps命令:显示当前进程的状态
运行jobs命令:用于显示Linux中的任务列表及任务状态,包括后台运行的任务。
运行fg命令:用于将后台作业(在后台运行的或者在后台挂起的作业)放到前台终端运行。由于后台作业只有hello,于是hello被转到前台运行,继续循环输出字符串。pstree命令:将所有行程以树状图显示,kill命令
kill -9 <进程号> :杀死对应进程

6.7本章小结

本章介绍了进程的定义与作用,对hello被加载、执行的过程进行分析,同时介绍shell的一般处理流程和作用,并且着重分析了调用fork 函数创建新进程,调用execve函数加载并执行hello,以及hello的异常与信号处理。

第7章 hello的存储管理

7.1 hello的存储器地址空间

1)逻辑地址:

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

(2)物理地址:

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

(3)虚拟地址:

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

(4)线性地址:

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

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

(1) CPU的段寄存器:

在CPU中,跟段有关的CPU寄存器一共有6个:cs,ss,ds,es,fs,gs,它们保存的是段选择符(或者叫段描述符)。而同时这六个寄存器每个都有一个对应的非编程寄存器,它们对应的非编程寄存器中保存的是段描述符。系统可以把同一个寄存器用于不同的目的,方法是先将其寄存器中的值保存到内存中,之后恢复。而在系统中最主要的是cs,ds,ss这三个寄存器。

(2)段描述符

段描述符就是保存在全局描述符表或者局部描述符表中,当某个段寄存器试图通过自己的段选择符获取对于的段描述符时,会将获取到的段描述符放到自己的非编程寄存器中,这样就不用每次访问段都要跑到内存中的段描述符表中获取。

(3)全局描述符表与局部描述符表

全局描述符表和局部描述符表保存的都是段描述符,记住要把段描述符和段选择符区别开来,保存在寄存器中的是段选择符,这个段选择符会到描述符表中获取对于的段描述符,然后将段描述符保存到对应寄存器的非编程寄存器中。

系统中每个CPU有属于自己的一个全局描述符表(GDT),其所在内存的基地址和其大小一起保存在CPU的gdtr寄存器中。其大小为64K,一共可保存8192个段描述符,不过第一个一般都会置空,也就是能保存8191个段描述符。第一个置空的原因是防止加电后段寄存器未经初始化就进入保护模式而使用GDT。

而对于局部描述符表,CPU设定是每个进程可以创建属于自己的局部描述符表(LDT),当前被使用的LDT的基地址和大小一起保存在ldtr寄存器中。不过大多数用户态的liunx程序都不使用局部描述符表,所以linux内核只定义了一个缺省的LDT供大多数进程共享。描述这个局部描述符表的局部描述符表描述符保存在GDT中。

(4)分段机制将逻辑地址转化为线性地址的步骤:

1)使用段选择符中的偏移值(段索引)在GDT或LDT表中定位相应的段描述符。(仅当一个新的段选择符加载到段寄存器中是才需要这一步)

2)利用段选择符检验段的访问权限和范围,以确保该段可访问。

3)把段描述符中取到的段基地址加到偏移量(也就是上述汇编语言汇中直接出现的操作地址)上,最后形成一个线性地址。

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

Linux采用了分页的方式来记录对应关系。所谓的分页,就是以更大尺寸的单位页来管理内存。在Linux中,通常每页大小为4KB。CPU中的一个控制寄存器,页表基址寄存器(Page Table Base Register, PTBR)指向当前页表。n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移(Virtual Page Offsetm, 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是相同的。下图展示了从虚拟地址到物理地址的基于页表的翻译过程:

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

每次CPU产生一个虛拟地址,MMU就必须查阅一个PTE,以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会要求从内存多取一次数据,代价是几十到几百个周期。

如果PTE碰巧缓存在L1中,那么开销就下降到1个或2个周期。然而,许多系统都试图消除即使是这样的开销,它们在MMU中包括了一个关于PTE的小

的缓存,称为翻译后备缓冲器(Translation Lookaside Buffer,TLB)。TLB是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块。TLB通常有高度的相联度。如图9-15 所示,用于组选择和行匹配的索引和标记字段是从虚拟地址中的虚拟页号中提取出来的。如果TLB有T=2^t个组,那么TLB索引(TLBI)是由VPN的t个最低位组成的,而TLB标记(TLBT)是由VPN中剩余的位组成的。

TLB具有如下特征:

MMU中一个小的相联存储设备

实现虚拟页码向物理页码的映射

对于页码数很少的页表可以完全包含在TLB中

当TLB命中时(通常情况)所包括的步骤。这里的关键点是,所有的地址翻译步骤都是在芯片上的MMU中执行的,因此非常快。

第1步:CPU产生一个虚拟地址。

第2步和第3步: MMU从TLB中取出相应的PTE。

第4步:MMU将这个虚拟地址翻译成–个物理地址,并且将它发送到高速缓存/主存。

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

当TLB不命中时,MMU必须从L1缓存中取出相应的PTE,如上图所示。新取出的PTE存放在TLB中,可能会覆盖一个已经存在的条目。

单级页表的局限性

在32位系统中,地址空间有32位,假设每个页面大小为4KB,每个PTE大小为4字节,那么即使所引用的只是虚拟地址空间中很小的一部分,也总是需要一个4MB的页表驻留在内存中,对于地址空间为64位的系统而言,问题将变得更加复杂。

为解决上述问题,我们使用层次结构的页表来对其空间进行压缩,其主要思想为:将页表构建出层次结构,高级页表中存储低级页表的低质,最底层页表存储相应的物理内存地址。

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

Linux为了在更高层次提供抽像,为每个CPU提供统一的界面。提供了一个四层页管理架构,来兼容这些二级、三级、四级管理架构的CPU。

这四级分别为:

①页全局目录PGD(对应刚才的页目录)

②页上级目录PUD(新引进的)

③页中间目录PMD(也就新引进的)

④页表PT(对应刚才的页表)。

每次对一个页进行了写之后,MMU都会设置D位,又称修改位或脏位。修改位告诉内核在复制替换页之前是否必须写回牺牲页。内核可以通过调用一条特殊的内核模式指令来清除引用位和修改位。

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

获得物理地址之后,先取出组索引对应位,在L1中寻找对应组。如果存在,则比较标志位,相等后检查有效位是否为1.如果都满足则命中取出值传给CPU,否则按顺序对L2cache、L3cache、内存进行相同操作,直到出现命中。然后再一级一级向上传,如果有空闲块则将目标块放置到空闲块中,否则将缓存中的某个块驱逐,将目标块放到被驱逐块的位置。

7.6 hello进程fork时的内存映射

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

7.7 hello进程execve时的内存映射

execve函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效地替代了当前程序。加载并运行hello需要以下几个步骤:

①删除已存在的用户区域

删除当前进程虚拟地址的用户部分中的已存在的区域结构。

②映射私有区域

为新程序的代码、数据、bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text和.data区,bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是请求二进制零的,初始长度为零。

③映射共享区域

hello程序与共享对象libc.so链接,libc.so是动态链接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。

④设置程序计数器(PC)

execve做的最后一件事情就是设置当前进程上下文的程序计数器,使之指向代码区域的入口点。

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

如果程序执行过程中发生了缺页故障,则内核调用缺页处理程序。

处理程序执行如下步骤:

1.检查虚拟地址是否合法,如果不合法则触发一个段错误,程序终止。

2.检查进程是否有读、写或执行该区域页面的权限,如果不具有则触发保护异常,

程序终止。

3.两步检查都无误后,内核选择一个牺牲页面,如果该页面被修改过则将其交换

出去,换入新的页面并更新页表。然后将控制转移给hello进程,再次执行触发缺页故障的指令。

7.9动态存储分配管理

1)动态内存分配器的基本原理

动态内存分配器维护着一个进程的虚拟内存区域,称为堆。系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长。对于每个进程,内核维护着一个变量brk,它指向堆的顶部。

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

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

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

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

(2)带边界标签的隐式空闲链表分配器原理

对于带边界标签的隐式空闲链表分配器,一个块是由一个字的头部、有效载

荷、可能的一些额外的填充,以及在块的结尾处的一个字的脚部组成的。

头部编码了这个块的大小(包括头部和所有的填充),以及这个块是已分配的还是空闲的。如果我们强加一个双字的对齐约束条件,那么块大小就总是8的倍数,且块大小的最低3位总是0。因此,我们只需要内存大小的29个高位,释放剩余的3位来编码其他信息。在这种情况中,我们用其中的最低位(已分配位)来指明这个块是已分配的还是空闲的。

头部后面就是应用调用malloc时请求的有效载荷。有效载荷后面是一片不使用的填充块,其大小可以是任意的。需要填充有很多原因。比如,填充可能是分配器策略的一部分,用来对付外部碎片。或者也需要用它来满足对齐要求。

我们称这种结构称为隐式空闲链表,是因为空闲块是通过头部中的大小字段隐含地连接着的。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。注意:此时我们需要某种特殊标记的结束块,可以是一个设置了已分配位而大小为零的终止头部。

(3)显式空间链表的基本原理

因为根据定义,程序不需要一个空闲块的主体,所以实现空闲链表数据结构的指针可以存放在这些空闲块的主体里面。

显式空闲链表结构将堆组织成一个双向空闲链表,在每个空闲块的主体中,都包含一个pred(前驱)和succ(后继)指针。

使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。不过,释放一个块的时间可以是线性的,也可能是个常数,这取决于空闲链表中块的排序策略。

一种方法是用后进先出(LIFO)的顺序维护链表,将新释放的块放置在链表的开始处。另一种方法是按照地址顺序来维护链表,其中链表中每个块的地址都小于它后继的地址

7.10本章小结

在本章中整理了有关内存管理的知识,介绍了四种地址空间,以及intel环境下的段式管理和页式管理,同时以Intel i7处理器为例,介绍了基于四级页表、三级cache的虚拟地址空间到物理地址的转换,阐述了forkexceve的内存映射,并介绍缺页故障和缺页中断管理机制。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:所有IO设备都被模型化为文件,所有的输入和输出都能被当做相应文件的读和写来执行。
设备管理:Linux内核有一个简单、低级的接口,成为Unix I/O,是的所有的输入和输出都能以一种统一且一致的方式来执行。

8.2 简述Unix IO接口及其函数

Unix IO接口:

1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。

2.Linux Shell创建的每个进程都有三个打开的文件:标准输入(描述符为0),标准输出(描述符为1),标准错误(描述符为2)。

3.改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。

4.读写文件。一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。

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

Unix I/O函数:

①int open(char* filename,int flags,mode_t mode)

进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,mode参数指定了新文件的访问权限位。

②int close(fd)

进程通过调用close函数关闭一个打开的文件,fd是需要关闭的文件的描述符。

③ssize_t read(int fd,void *buf,size_t n)

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

④ssize_t wirte(int fd,const void *buf,size_t n)

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

8.3 printf的实现分析

printf需要做的事情是:接受一个fmt的格式,然后将匹配到的参数按照fmt格式输出。printf用了两个外部函数,一个是vsprintf,还有一个是write。

vsprintf函数作用是接受确定输出格式的格式字符串fmt(输入)。用格式字符串对个数变化的参数进行格式化,产生格式化输出。

write函数将buf中的i个元素写到终端。

从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall。字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

getchar有一个int型的返回值。当程序调用getchar时,程序就等着用户按键,用户输入的字符被存放在键盘缓冲区中直到用户按回车为止(回车字符也放在缓冲区中)。

当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的第一个字符的ASCII码,如出错返回-1,且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。

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

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

8.5本章小结

本章主要介绍了 Linux 的 IO 设备管理方法、Unix IO 接口及其函数,分析了 printf 函数和 getchar 函数的实现。

结论

1.输入:将hello.c代码从键盘输入。

2.预处理(cpp):将hello.c进行预处理,将c文件调用的所有外部的库展开合并,

生成hello.i文件。

3.编译(ccl):将hello.i文件进行翻译生成汇编语言文件hello.s。

4.汇编(as):将hello.s翻译成一个可重定位目标文件hello.o。

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

序hello,至此可执行hello程序正式诞生。

6.运行:在shell中输入./hello 1190201016 石衍 1

7.创建子进程:由于终端输入的不是一个内置的shell命令,因此shell调用fork ()

函数创建一个子进程。

8.加载程序:shell调用execve函数,启动加载器,映射虚拟内存,进入程序入口

后程序开始载入物理内存,然后进入main函数。

9.执行指令:CPU为进程分配时间片,在一个时间片中,hello享有CPU资源, 顺

序执行自己的控制逻辑流。

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

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

12.信号管理:当程序在运行的时候我们输入Ctrl+c,内核会发送SIGINT信号给进

程并终止前台作业。当输入Ctrl+z时,内核会发送SIGTSTP信号给进程,并将前台作业停止挂起。

13终止:当子进程执行完成时,内核安排父进程回收子进程,将子进程的退出状态传递给父进程。内核删除为这个进程创建的所有数据结构。

附件

列出所有的中间产物的文件名,并予以说明起作用。

hello.i 预处理之后的程序(文本)

hello.s 汇编语言程序(文本)

hello.o 可重定位目标程序(二进制)

hello 可执行目标程序(二进制)

hello_asm.txt   hello.o的反汇编文件

hello1_asm.txt   hello的反汇编文件

hello_elf.txt hello.o的elf头信息

hello1_elf.txt 可执行文件hello的elf头信息

参考文献

[1] Randal E.Bryant / David O’Hallaron. 深入理解计算机系统(原书第3版)[M]. 机械工业出版社,2016:1-87.

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值