Hello的一生

摘 要

hello的一生经历了许多变动和成长。从键入hello.c文件,到预处理生成hello.i文件、编译生成hello.s汇编文件、汇编生成hello.o可重定位目标文件、链接生成hello可执行文件。经历了这么多,终于生成可以执行的文件了。接下来在shell中运行,shell为其创建子进程,调用execve,execve调用启动加载器,加映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入 main函数。执行,CPU为其分配时间片执行。访问内存,将虚拟内存地址映射成物理地址执行。Printf调用malloc分配内存。接下来输入信号可将其挂起或停止。最后运行结束,hello被回收。hello结束了艰辛的一生。

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

第1章 概述

1.1 Hello简介

在linux中,hello.c经过cpp的预处理、ccl的编译、as的汇编、ld的链接最终成为可执行目标程序hello。
在shell中键入启动命令后,shell为其fork,产生子进程,于是hello便从Program摇身一变成为Process,这便是P2P的过程。
之后shell为其execve,映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入 main函数执行目标代码,CPU为运行的hello分配时间片执行逻辑控制流。当程序运行结束后,shell父进程负责回收hello进程,内核删除相关数据结构,以上全部便是020的过程。

1.2 环境与工具

1.2.1硬件环境
X64 CPU;2GHz;2G RAM;256GHD Disk 以上
1.2.2 软件环境
Windows7 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位/优麒麟 64位;
1.2.3开发工具
Visual Studio 2010 64位以上;GDB/OBJDUMP;DDD/EDB等

1.3 中间结果

列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.i(预处理后的文件):预处理阶段实现的功能主要有三个:1.加载头文件2.进行宏替换3.条件编译
hello.s(汇编程序):包含汇编语言程序。
hello.o(可重定位目标程序):将汇编语言翻译成机器语言指令,并将指令打包成一种叫做可重定位目标程序的格式。

1.4 本章小结

本章主要介绍的hello的简介、环境与工具、中间结果。为作业做准备。

第2章 预处理

2.1 预处理的概念与作用

预编译器(cpp)根据以字符#开头的命令(条件编译,宏定义),将头文件stdio.h的内容直接插入到Hello.c文件中,最终的得到一个以i为扩展名的C文件—Hello.i文件。
作用:
1.删除”#define”并展开所定义的宏
2.处理所有条件预编译指令,如”#if”,”#ifdef”,”#endif”等
3.插入头文件到”#include”处,可以递归方式进行处理
4.删除所有的注释”//”和”/**/”
5.添加行号和文件名标识,以便编译时编译器产生调试用的行号信息
6.保留所有#pragma编译指令(编译器需要用)
经过预编译处理后,得到的是预处理文件(如,hello.i) ,它还 是一个可读的文本文件,但不包含任何宏定义

2.2在Ubuntu下预处理的命令

在Ubuntu下输入gcc –E hello.c –o hello.i或者cpp hello.c > hello.i
图2.1 预处理生成hello.i文件
图2.1 预处理生成hello.i文件

2.3 Hello的预处理结果解析

在终端中输入vim hello.i(用vim打开hello.i文件)。此时hello.i文件已经有3118行代码了,main函数在最下面。
图2.2 hello.i文件中的main函数
图2.2 hello.i文件中的main函数

在main函数之前是stdio.h unistd.h stdlib.h三个头文件的展开,如stdio.h头文件,预编译器到默认的环境变量下寻找stdio.h,发现其中使用了#define语句,预编译器对此进行递归展开,因此.i程序中是没有#define等宏定义的。并且使用了大量的#ifdef #ifndef的语句,预编译器会对条件值进行判断来决定是否执行包含其中的逻辑。

2.4 本章小结

本章主要介绍了预处理的过程,从.c文件生成预处理.i文件,并展开.i文件。

第3章 编译

3.1 编译的概念与作用

编译过程就是将预处理后得到的预处理文件(如hello.i)进行 词法分析、语法分析、语义分析、优化后,生成汇编代码文件。用来进行编译处理的程序称为编译程序(编译器,Compiler)。
编译器的流程如下:
1.词法分析器,用于将字符串转化成内部的表示结构。
2.语法分析器,将词法分析得到的标记流生成一棵语法树。
3.目标代码的生成,将语法树转化成目标代码。

3.2 在Ubuntu下编译的命令

在终端中输入gcc –S hello.i –o hello.s或者gcc –S hello.c –o hello.s
图3.1 编译生成.s文件
图3.1 编译生成.s文件

3.3 Hello的编译结果解析

3.3.1编译指令
.file:声明源文件
.text:代码段
.section .rodata:rodata节
.globl:声明一个全局变量
.type:用来指定是函数类型还是对象类型
.size:声明大小
.long、.string:声明一个long、string类型
.align:声明对指令或者数据的存放地址进行对齐的方式
3.3.2所用数据
编译生成的hello.s文件用到的数据类型:字符串,整数,数组
程序中有两个字符串类型:
图3.2 hello.s中字符串
图3.2 hello.s中字符串

两个字符串都在.rodata只读数据段中。
程序中的整数数据如下:
图3.3 hello.s中整数
图3.3 hello.s中整数

  1. int sleepsecs:在C中sleepsecs声明为全局变量,并在.text节中声明该全局变量。在.data节中赋值等。设置类型为对象,对齐方式为4,定义为long类型的2。
  2. int i:i为局部变量存储在寄存器或者栈空间中,在hello.s中编译器将i存储在栈上空间-4(%rbp)中。
  3. int argv:作为参数传入。
  4. 立即数:直接写到汇编代码中。
    程序中涉及的数组为char *argv[] main。
    图3.4 hello.s中的数组
    图3.4 hello.s中的数组

argv单个元素char*大小为8B,在文件中起始地址为argv,main函数中访问数组元素argv[1],argv[2]时,按照起始地址argv大小8B计算数据地址取数据。
3.3.3类型转换
程序中涉及隐式类型转换的是:int sleepsecs=2.5,将浮点数类型的2.5转换为int类型。
3.3.4赋值

  1. int sleepsecs=2.5 :sleepsecs为全局变量,在.data节中声明。
    2.i = 0:用mov指令直接赋值。
    图3.5 hello.s中对i的赋值
    图3.5 hello.s中对i的赋值

3.3.5算数
程序中涉及的算数:

  1. i++ 计数器i增加,使用程序addl指令。

  2. 使用leaq .LC1(%rip),%rdi,使用了加载有效地址指令leaq计算LC1的段地址%rip+.LC1并传递给%rdi。
    3.3.6关系
    本程序中涉及的有:

  3. argc!=3:hello.s中使用cmpl $3,-20(%rbp),将argv-3设置为条件码,为之后的跳转设置条件。

  4. i<10:hello.s中使用cmpl $9,-4(%rbp),将i-9设置为条件码,为之后的跳转设置条件。
    3.3.7控制转移
    本程序中涉及的有:

  5. if语句:if (argv!=3),当argv不等于3的时候执行程序段中的代码。
    图3.6 hello.s文件中if语句
    图3.6 hello.s文件中if语句

  6. for语句:for(i=0;i<10;i++):使用计数变量i循环10次,首先无条件跳转到位于循环体.L4之后的比较代码,使用cmpl进行比较,如果i<=9,则跳入.L4 for循环体执行,否则说明循环结束,顺序执行for之后的逻辑。图3.7 hello.s文件中for语句图3.7 hello.s文件中for语句3.3.8函数操作

本程序涉及的有:

  1. main函数:
    传递控制,main函数因为被调用call才能执行(被系统启动函数__libc_start_main调用),call指令将下一条指令的地址dest压栈,然后跳转到main函数。传递数据,外部调用过程向main函数传递参数argc和argv,分别使用%rdi和%rsi存储,函数正常出口为return 0,将%eax设置0返回。分配和释放内存,使用%rbp记录栈帧的底,函数分配栈帧空间在%rbp之上,程序结束时,调用leave指令,leave相当于mov %rbp,%rsp,pop %rbp,恢复栈空间为调用之前的状态,然后ret返回,ret相当pop IP,将下一条要执行指令的地址设置为dest。
  2. printf函数:
    传递数据:第一次printf将%rdi设置为“Usage: Hello 学号 姓名!\n”字符串的首地址。第二次printf设置%rdi为“Hello %s %s\n”的首地址,设置%rsi为argv[1],%rdx为argv[2]。控制传递:第一次printf因为只有一个字符串参数,所以call puts@PLT;第二次printf使用call printf@PLT。
  3. exit函数:
    传递数据:将%edi设置为1。控制传递:call exit@PLT。
  4. sleep函数:
    传递数据:将%edi设置为sleepsecs。控制传递:call sleep@PLT。
  5. getchar函数:
    控制传递:call gethcar@PLT

3.4 本章小结

本章主要介绍了经预处理得到的.i文件进一步进行编译得到.s文件。生成的.s文件用汇编语言生成

第4章 汇编

4.1 汇编的概念与作用

汇编代码文件(由汇编指令构成)称为汇编语言源程序,汇编程序(汇编器)用来将汇编语言源程序转换为机器指令序列(机器语言程序),汇编指令和机器指令一一对应,前者是后者的符号表示,它们都属于机器级指令,所构成的程序称为机器级代码。汇编结果是一个可重定位目标文件(如,hello.o),其中包含的是不可读的二进制代码,必须用相应的工具软件来查看其内容

4.2 在Ubuntu下汇编的命令

在终端中输入:gcc –c hello.s –o hello.o或者gcc –c hello.c –o hello.o或者as hello.s -o hello.o图4.1 生成hello.o文件
图4.1 生成hello.o文件

4.3 可重定位目标elf格式

在终端中输入readelf -a hello.o > helloo.elf得到ELF格式的文件:

  1. ELF头文件:
    图4.2 ELF头信息
    图4.2 ELF头信息

该区域以Magic开始,有类别,数据,版本等信息,都在ELF头中。

  1. 节头部表
    图4.3 节头部表信息
    图4.3 节头部表信息

其中包含了包含了文件中出现的各个节的语义,包括节的类型、位置和大小等信息。
3. 重定位节.rela.text
图4.4 重定位节.rela.text的信息图4.4 重定位节.rela.text的信息
图4.4 重定位节.rela.text的信息

包含.text节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。图中8条重定位信息分别是对.L0(第一个printf中的字符串)、puts函数、exit函数、.L1(第二个printf中的字符串)、printf函数、sleepsecs、sleep函数、getchar函数进行重定位声明
4…symtab

图4.5 .symtab信息
图4.5 .symtab信息

该节信息存放函数和全局变量(符号表)信息,它不包括局部变量。

4.4 Hello.o的结果解析

在终端中输入objdump -d -r hello.o得到反汇编代码:图4.6 hello.o反汇编代码
图4.6 hello.o反汇编代码
图4.6 hello.o反汇编代码

.o文件与.s文件的区别不是很大,主要差别如下:
分支转移:反汇编代码跳转指令的操作数使用的不是段名称,而是确定的地址。
函数调用:在.s文件中,函数调用之后直接跟着函数名称,而在反汇编程序中,call的目标地址是当前下一条指令。这是因为hello.c中调用的函数都是共享库中的函数,最终需要通过动态链接器才能确定函数的运行时执行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其call指令后的相对地址设置为全0(目标地址正是下一条指令),然后在.rela.text节中为其添加重定位条目,等待静态链接的进一步确定。
全局变量访问:在.s文件中,访问rodata,使用段名称+%rip,在反汇编代码中0+%rip,因为rodata中数据地址也是在运行时确定,访问时需要重定位。所以在汇编成为机器语言时,将操作数设置为全0并添加重定位条目。

4.5 本章小结

本章主要讲解了从hello.s文件到hello.o文件的转变。查看hello.o文件的ELF头信息,将hello.o文件进行反汇编与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.o hello.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o命令
图5.1 链接得到的文件
图5.1 链接得到的文件

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

在终端中输入readelf -a hello > hello.elf生成hello的ELF文件。
图5.2 hello中的ELF头信息
图5.2 hello中的ELF头信息
图5.2 hello中的ELF头信息

该图中已经给出类各节的大小,偏移量等信息,在此不一一列出。

5.4 hello的虚拟地址空间

使用edb加载hello,通过edb的data dump窗口可看到加载到虚拟内存的hello程序。
虚拟地址从0x400000开始,在0x400fff结束。
图5.3 edb中data dump窗口的首尾
图5.3 edb中data dump窗口的首尾
图5.3 edb中data dump窗口的首尾

查看elf的程序头表:
图5.4 elf的程序头表
图5.4 elf的程序头表

此图中的virtaddr为虚拟地址。
程序包含8个段:
PHDR保存程序头表。
INTERP指定在程序已经从可执行文件映射到内存之后,必须调用的解释器(如动态链接器)。
LOAD表示一个需要从二进制文件映射到虚拟地址空间的段。其中保存了常量数据(如字符串)、程序的目标代码等。
DYNAMIC保存了由动态链接器使用的信息。
NOTE保存辅助信息。
GNU_STACK:权限标志,标志栈是否是可执行的。
GNU_RELRO:指定在重定位结束之后那些内存区域是需要设置只读。

5.5 链接的重定位过程分析

在终端中输入objdump -d -r hello > hello.txt得到反汇编代码:
图5.5 hello文件的反汇编代码
图5.5 hello文件的反汇编代码
图5.5 hello文件的反汇编代码
图5.5 hello文件的反汇编代码
图5.5 hello文件的反汇编代码
图5.5 hello文件的反汇编代码

反汇编文本比.o文件多了许多节。如下:
.interp:保存ld.so的路径
.note.ABI-tag:Linux下特有的section
.hash:符号的哈希表
.gnu.hash:GNU拓展的符号的哈希表
.dynsym:运行时/动态符号表
.dynstr:存放.dynsym节中的符号名称
.gnu.version:符号版本
.gnu.version_r:符号引用版本
.rela.dyn:运行时/动态重定位表
.rela.plt:.plt节的重定位条目
.init:程序初始化需要执行的代码
.plt:动态链接-过程链接表
.fini:当程序正常终止时需要执行的代码
.eh_frame:contains exception unwinding and source language information.
.dynamic:存放被ld.so使用的动态链接信息
.got:动态链接-全局偏移量表-存放变量
.got.plt:动态链接-全局偏移量表-存放函数
.data:初始化了的数据
.comment:一串包含编译器的NULL-terminated字符串
还有一些不同:
函数个数:在使用ld命令链接的时候,指定了动态链接器为64的/lib64/ld-linux-x86-64.so.2,crt1.o、crti.o、crtn.o中主要定义了程序入口_start、初始化函数_init,_start程序调用hello.c中的main函数,libc.so是动态链接共享库,其中定义了hello.c中用到的printf、sleep、getchar、exit函数和_start中调用的__libc_csu_init,__libc_csu_fini,__libc_start_main。链接器将上述函数加入。
函数调用:链接器解析重定条目时发现对外部函数调用的类型为R_X86_64_PLT32的重定位,此时动态链接库中的函数已经加入到了PLT中,.text与.plt节相对距离已经确定,链接器计算相对距离,将对动态链接库中函数的调用值改为PLT中相应函数与下条指令的相对地址,指向对应函数。对于此类重定位链接器为其构造.plt与.got.plt。
.rodata引用:链接器解析重定条目时发现两个类型为R_X86_64_PC32的对.rodata的重定位(printf中的两个字符串),.rodata与.text节之间的相对距离确定,因此链接器直接修改call之后的值为目标地址与下一条指令的地址之差,指向相应的字符串。

5.6 hello的执行流程

使用edb执行hello,主要程序名称和程序地址如下:
ld-2.27.so!_dl_start:0x7fce 8cc38ea0
ld-2.27.so!_dl_init:0x7fce 8cc47630
hello!_start:0x400500
libc-2.27.so!__libc_start_main:0x7fce 8c867ab0
-libc-2.27.so!__cxa_atexit:0x7fce 8c889430
-libc-2.27.so!__libc_csu_init:0x4005c0
hello!_init:0x400488
libc-2.27.so!_setjmp:0x7fce 8c884c10
-libc-2.27.so!_sigsetjmp:0x7fce 8c884b70
–libc-2.27.so!__sigjmp_save:0x7fce 8c884bd0
hello!main:0x400532
hello!puts@plt:0x4004b0
hello!exit@plt:0x4004e0
*hello!printf@plt:–
*hello!sleep@plt:–
*hello!getchar@plt:–
ld-2.27.so!_dl_runtime_resolve_xsave:0x7fce 8cc4e680
-ld-2.27.so!_dl_fixup:0x7fce 8cc46df0
–ld-2.27.so!_dl_lookup_symbol_x:0x7fce 8cc420b0
libc-2.27.so!exit:0x7fce 8c889128

5.7 Hello的动态链接分析

对于动态共享链接库中PIC函数,编译器没有办法预测函数的运行时地址,所以需要添加重定位记录,等待动态链接器处理,为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。
在调用dl_init之前,调用的地址都是指向PLT中的代码逻辑,如图:
图5.6 调用dl_init之前
图5.6 调用dl_init之前

在调用dl_init之后,0x601008和0x601010处的两个8B数据分别发生改变为0x7fda 9fd3f170和0x7fda 9fb2d680。
图5.7 调用dl_init之后
图5.7 调用dl_init之后
图5.8 0x7fda 9fd3f170指向的重定位表
图5.8 0x7fda 9fd3f170指向的重定位表
图5.9 0x7fda 9fb2d680处目标程序动态器
图5.9 0x7fda 9fb2d680处目标程序动态器

5.8 本章小结

在本章中主要介绍了链接的概念与作用,hello的ELF格式,分析了hello的虚拟地址空间、重定位过程、执行流程、动态链接过程等。

第6章 hello进程管理

6.1 进程的概念与作用

进程的概念:一个正在运行的程序的实例。一个具有一定独立功能的程序关于某个数据集合的一次运行活动,是系统进行资源分配和调度运行的基本单位进程是一种动态描述,但是并不代表所有的进程都在运行。进程有两个基本元素,一个是程序代码(可能被执行相同程序的其他程序共享)和代码相关联的数据集。
作用:
PCB 可以被操作系统中的多个模块读或修改,如被调度程序、资源分配 程序、中断处理程序以及监督和分析程序等读或修改。OS是根据 PCB来对 并发执行的进程进行控制和管理是操作系统中最重要的记录型数据结构Linux 系统中用 task_struct 数据结构,
Windows:执行体进程块(EPROCESS)使一个在多道程序环境下不能独立运行的程序(含数据),成为一个能独立运行的基本单位,
一个能与其它进程并发执行的进程
1、作为独立运行基本单位的标志
2、能实现间断性运行方式
3、提供进程管理所需要的信息
4、提供进程调度所需要的信息
5、实现与其他进程的同步与通信

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

作用:1. 可交互,和非交互的使用shell。在交互式模式,shell从键盘接收输入;在非交互式模式,shell从文件中获取输入。2. shell中可以同步和异步的执行命令。在同步模式,shell要等命令执行完,才能接收下面的输入。在异步模式,命令运行的同时,shell就可接收其它的输入。重定向功能,可以更细致的控制命令的输入输出。另外,shell允许设置命令的运行环境。3. shell提供了少量的内置命令,以便自身功能更加完备和高效。4. shell除了执行命令,还提供了变量,流程控制,引用和函数等,类似高级语言一样,能编写功能丰富的程序。5. shell强大的的交互性除了可编程,还体现在作业控制,命令行编辑,历史命令,和别名等方面。
处理流程:1.Shell首先从命令行中找出特殊字符(元字符),在将元字符翻译成间隔符号。元字符将命令行划分成小块tokens。Shell中的元字符如下所示:
SPACE , TAB , NEWLINE , & , ; , ( , ) ,< , > , |
2. 程序块tokens被处理,检查看他们是否是shell中所引用到的关键字。
3. 当程序块tokens被确定以后,shell根据aliases文件中的列表来检查命令的第一个单词。如果这个单词出现在aliases表中,执行替换操作并且处理过程回到第一步重新分割程序块tokens。
4.Shell对~符号进行替换。
5.Shell对所有前面带有 符 号 的 变 量 进 行 替 换 。 6 . S h e l l 将 命 令 行 中 的 内 嵌 命 令 表 达 式 替 换 成 命 令 ; 他 们 一 般 都 采 用 符号的变量进行替换。 6.Shell将命令行中的内嵌命令表达式替换成命令;他们一般都采用 6Shell(command)标记法。
7.Shell计算采用$(expression)标记的算术表达式。
8.Shell将命令字符串重新划分为新的块tokens。这次划分的依据是栏位分割符号,称为IFS。缺省的IFS变量包含有:SPACE , TAB 和换行符号。
9.Shell执行通配符* ? [ ]的替换。
10.shell把所有從處理的結果中用到的注释删除,並且按照下面的顺序实行命令的检查:
A. 内建的命令
B. shell函数(由用户自己定义的)
C. 可执行的脚本文件(需要寻找文件和PATH路径)
11.在执行前的最后一步是初始化所有的输入输出重定向。
12.最后,执行命令。

6.3 Hello的fork进程创建过程

hello不是一个内置的shell命令所以解析之后终端程序判断./hello的语义为执行当前目录下的可执行目标文件hello,之后终端程序首先会调用fork函数创建一个新的运行的子进程,新创建的子进程几乎但不完全与父进程相同,子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,这就意味着,当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程与子进程之间最大的区别在于它们拥有不同的PID。
图6.1 进程图
图6.1 进程图

6.4 Hello的execve过程

当fork之后,子进程调用execve函数(传入命令行参数)在当前进程的上下文中加载并运行一个新程序即hello程序,execve调用驻留在内存中的被称为启动加载器的操作系统代码来执行hello程序,加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零,通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件中的内容。最后加载器设置PC指向_start地址,_start最终调用hello中的main函数。除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制。直到CPU引用一个被映射的虚拟页时才会进行复制,这时,操作系统利用它的页面调度机制自动将页面从磁盘传送到内存
图6.2 系统映像
图6.2 系统映像

6.5 Hello的进程执行

逻辑控制流:一系列程序计数器PC的值的序列叫做逻辑控制流,进程是轮流使用处理器的,在同一个处理器核心中,每个进程执行它的流的一部分后被抢占(暂时挂起),然后轮到其他进程。
用户模式和内核模式:处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
简单看hello sleep进程调度的过程:当调用sleep之前,如果hello程序不被抢占则顺序执行,假如发生被抢占的情况,则进行上下文切换,上下文切换是由内核中调度器完成的,当内核调度新的进程运行后,它就会抢占当前进程,并进行1)保存以前进程的上下文2)恢复新恢复进程被保存的上下文,3)将控制传递给这个新恢复的进程 ,来完成上下文切换。
图6.3 hello进程sleep上下文切换
图6.3 hello进程sleep上下文切换

6.6 hello的异常与信号处理

hello执行过程中会出现的异常种类有:
1.中断:SIGSTP:挂起程序
2.终止:SIGINT:终止程序
图7.4 正常运行
图7.4 正常运行
图7.5 PS命令
图7.5 PS命令
图7.6 中途按下Ctrl Z
图7.6 中途按下Ctrl Z
图7.7 中途按下Ctrl C
图7.7 中途按下Ctrl C
图7.8 乱按
图7.8 乱按
图7.8 乱按
图7.8 乱按
图7.8 乱按

6.7本章小结

在本章中,阐明了进程的定义与作用,介绍了Shell的一般处理流程,调用fork创建新进程,调用execve执行hello,hello的进程执行,hello的异常与信号处理。

第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:程序代码经过编译后出现在汇编程序中地址。逻辑地址由选择符和偏移量组成。
线性地址:逻辑地址经过段机制后转化为线性地址,为描述符:偏移量的组合形式。分页机制中线性地址作为输入。
虚拟地址 (virtual address): CPU启动保护模式后,程序运行在虚拟地址空间中。注意,并不是所有的“程序”都是运行在虚拟地址中。CPU在启动的时候是运行在实模式的,Bootloader以及内核在初始化页表之前并不使用虚拟地址,而是直接使用物理地址的。
物理地址 (physical address): 放在寻址总线上的地址。放在寻址总线上,如果是读,电路根据这个地址每位的值就将相应地址的物理内存中的数据放到数据总线中传输。如果是写,电路根据这个地址每位的值就在相应地址的物理内存中放入数据总线上的内容。物理内存是以字节(8位)为单位编址的。
图7.1 逻辑地址、线性地址、物理地址三者关系
图7.1 逻辑地址、线性地址、物理地址三者关系

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

分段功能在实模式和保护模式下有所不同。
实模式,即不设防,也就是说逻辑地址=线性地址=实际的物理地址。段寄存器存放真实段基址,同时给出32位地址偏移量,则可以访问真实物理内存。
在保护模式下,线性地址还需要经过分页机制才能够得到物理地址,线性地址也需要逻辑地址通过段机制来得到。段寄存器无法放下32位段基址,所以它们被称作选择符,用于引用段描述符表中的表项来获得描述符。
Base:基地址,32位线性地址指向段的开始。Limit:段界限,段的大小。 DPL:描述符的特权级0(内核模式)-3(用户模式)。
所有的段描述符被保存在两个表中:全局描述符表GDT和局部描述符表LDT。gdtr寄存器指向GDT表基址。
在保护模式下,分段机制就可以描述为:通过解析段寄存器中的段选择符在段描述符表中根据Index选择目标描述符条目Segment Descriptor,从目标描述符中提取出目标段的基地址Base address,最后加上偏移量offset共同构成线性地址Linear Address。
图7.2 段式管理
图7.2 段式管理

下面是转换的具体步骤:
给定一个完整的逻辑地址[段选择符:段内偏移地址]。
看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。可以得到一个数组。
取出段选择符中前13位,在数组中查找到对应的段描述符,得到Base,也就是基地址。
线性地址 = Base + offset。

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

线性地址被分为以固定长度为单位的组,称为页(page),例如一个32位的机器,线性地址最大可为4G,可以用4KB为一个页来划分,这页,整个线性地址就被划分为一个tatol_page[220]的大数组,共有2的20个次方个页。这个大数组我们称之为页目录。目录中的每一个目录项,就是一个地址——对应的页的地址。另一类“页”,我们称之为物理页,或者是页框、页桢的。是分页单元把所有的物理内存也划分为固定长度的管理单位,它的长度一般与内存页是一一对应的。这里注意到,这个total_page数组有220个成员,每个成员是一个地址(32位机,一个地址也就是4字节),那么要单单要表示这么一个数组,就要占去4MB的内存空间。为了节省空间,引入了一个二级管理模式的机器来组织分页单元。
图7.2 页式管理
图7.2 页式管理

如上图,
1、分页单元中,页目录是唯一的,它的地址放在CPU的cr3寄存器中,是进行地址转换的开始点。万里长征就从此长始了。
2、每一个活动的进程,因为都有其独立的对应的虚似内存(页目录也是唯一的),那么它也对应了一个独立的页目录地址。——运行一个进程,需要将它的页目录地址放到cr3寄存器中,将别个的保存下来。
3、每一个32位的线性地址被划分为三部份,面目录索引(10位):页表索引(10位):偏移(12位)
依据以下步骤进行转换:
1、从cr3中取出进程的页目录地址(操作系统负责在调度进程的时候,把这个地址装入对应寄存器);
2、根据线性地址前十位,在数组中,找到对应的索引项,因为引入了二级管理模式,页目录中的项,不再是页的地址,而是一个页表的地址。(又引入了一个数组),页的地址被放到页表中去了。
3、根据线性地址的中间十位,在页表(也是数组)中找到页的起始地址;
4、将页的起始地址与线性地址中最后12位相加,得到最终我们想要的

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

Core i7 MMU 如何使用四级的页表来将虚拟地址翻译成物理地址。36位VPN 被划分成四个9 位的片,每个片被用作到一个页表的偏移量。CR3 寄存器包含Ll页表的物理地址。VPN 1 提供到一个Ll PET 的偏移量,这个PTE包含L2 页表的基地址。VPN 2提供到一个L2 PTE的偏移量,以此类推。
图7.3 TLB与四级页表支持下的VA到PA的变换
图7.3 TLB与四级页表支持下的VA到PA的变换

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

我们要先将高速缓存与地址翻译结合起来,首先是CPU发出一个虚拟地址给TLB里面搜索,如果命中的话就直接先发送到L1cache里面,没有命中的话就先在页表里面找到以后再发送过去,到了L1里面以后,寻找物理地址又要检测是否命中,这里就是使用到我们的CPU的高速缓存机制了,通过这种机制再搭配上TLB就可以使得机器在翻译地址的时候的性能得以充分发挥。
图7.4 Core i7的内存系统
图7.4 Core i7的内存系统
图7.5 Core i7地址翻译的概况。为了简化,没有显示i-cache、i-TLB和L2统一TLB
图7.5 Core i7地址翻译的概况。为了简化,没有显示i-cache、i-TLB和L2统一TLB

7.6 hello进程fork时的内存映射

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

7.7 hello进程execve时的内存映射

加载并运行hello需要以下几个步骤:
1.删除已存在的用户区域。删除shell虚拟地址的用户部分中的已存在的区域结构。
2.映射私有区域。为hello的代码、数据、bss 和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello 文件中的.text和.data 区。bss 区域是请求二进制零的,映射到匿名文件,其大小包含在hello 中。栈和堆区域也是请求二进制零的,初始长度为零。图7.7 概括了私有区域的不同映射。
3.映射共享区域。如果hello程序与共享对象(或目标)链接,比如标准C 库libc. so, 那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
4.设置程序计数器(PC) 。execve 做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。

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

在虚拟内存的习惯说法中, DRAM 缓存不命中称为缺页(page fault) 。图7-7 展示了在缺页之前我们的示例页表的状态。CPU 引用了VP 3 中的一个字, VP 3 并未缓存在DRAM 中。地址翻译硬件从内存中读取PTE 3, 从有效位推断出VP 3 未被缓存,并且触发一个缺页异常。缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,在此例中就是存放在PP3中的VP4 。如果VP4已经被修改了,那么内核就会将它复制回磁盘。无论哪种情况,内核都会修改VP4 的页表条目,反映出VP 4 不再缓存在主存中这一事实。
图7.6 缺页异常示意图
图7.6 缺页异常示意图

7.9动态存储分配管理

动态内存分配器维护着一一个进程的虚拟内存区域,称为堆。系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长(向更高的地址)。对于每个进程,内核维护着一个变量brk(读做“break"),它指向堆的顶部。
分配器将堆视为一-组不同大小的块(block)的集合来维护。每个块就是一个连续的虚拟内存片(chunk),要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器有两种基本风格。两种风格都要求应用显式地分配块。它们的不同之处在于由哪个实体来负责释放已分配的块。
显式分配器(explicit allocator),要求应用显式地释放任何已分配的块。例如,C标准库提供- ~种叫做malloc程序包的显式分配器。C程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块。C++中的new和delete操作符与C中的malloc和free相当。
隐式分配器(implicit allocator),另一方面,要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器(garbage collec-tor), 而自动释放未使用的已分配的块的过程叫做垃圾收集( garbage collection)。例如,诸如Lisp、ML以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。
基本方法:在合并块的时候使用到的方法,有最佳适配和第二次适配还有首次适配方法,首次适配就是指的是第一次遇到的就直接适配分配,第二次顾名思义就是第二次适配上的,最佳适配就是搜索完以后最佳的方案,当然这种的会在搜索速度上大有降低。

7.10本章小结

本章通过对hello在储存结构,高速缓存,虚拟内存涉及到的方面进行了详细的探索,通过对这些结构的了解我们可以以后编写一些对高速缓存友好的代码,或者说运行速度更快的代码,对我们来说都是受益匪浅。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件
设备管理:unix io接口
所有的I/ O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux 内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行,这就是Unix I/O接口。

8.2 简述Unix IO接口及其函数

Unix I/O接口:
1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访间一个I/O 设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
2.Linux shell创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0)、标准输出(描述符为1)和标准错误(描述符为2)。头文件<unistd.h> 定义了常量STDIN_FILENO、STOOUT_FILENO和STDERR_FILENO,它们可用来代替显式的描述符值。
3.改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k, 初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek 操作,显式地设置文件的当前位置为K。
4.读写文件。一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n 。给定一个大小为m 字节的文件,当k~m 时执行读操作会触发一个称为end-of-file(EOF) 的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF 符号”。类似地,写操作就是从内存复制n>0 个字节到一个文件,从当前文件位置k开始,然后更新k 。
5.关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
Unix I/O函数:
1.进程是通过调用open函数来打开一个已存在的文件或者创建一个新文件的:
int open(char *filename, int flags, mode_t mode);
open 函数将filename 转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags 参数指明了进程打算如何访问这个文件,mode 参数指定了新文件的访问权限位。
返回:若成功则为新文件描述符,若出错为-1。
2.进程通过调用close 函数关闭一个打开的文件。
int close(int fd);
返回:若成功则为0, 若出错则为-1。
3.应用程序是通过分别调用read 和write 函数来执行输入和输出的。
ssize_t read(int fd, void *buf, size_t n);
read 函数从描述符为fd 的当前文件位置复制最多n 个字节到内存位置buf 。返回值-1表示一个错误,而返回值0 表示EOF。否则,返回值表示的是实际传送的字节数量。
返回:若成功则为读的字节数,若EOF 则为0, 若出错为-1。
ssize_t write(int fd, const void *buf, size_t n);
write 函数从内存位置buf 复制至多n 个字节到描述符fd 的当前文件位置。图10-3 展示了一个程序使用read 和write 调用一次一个字节地从标准输入复制到标准输出。
返回:若成功则为写的字节数,若出错则为-1。

8.3 printf的实现分析

printf函数:

static int printf(const char *fmt, ...)
{
     va_list args;
     int i;
   
     va_start(args, fmt);
     write(1,printbuf,i=vsprintf(printbuf, fmt, args));
     va_end(args);
     return i;
}

参数中明显采用了可变参数的定义,可以看到*fmt是一个char 类型的指针,指向字符串的起始位置。
va函数:

va_list arg_ptr;
 
void va_start( va_list arg_ptr, prev_param );
type va_arg( va_list arg_ptr, type );
void va_end( va_list arg_ptr );

查看vsprintf代码:

int vsprintf(char *buf, const char *fmt, va_list args)
{
    int len;
    int i;
    char * str;
    char *s;
    int *ip;
    int flags;            /* flags to number() */
    int field_width;      /* width of output field */
    int precision;        /* min. # of digits for integers; max number of chars for from string */
    int qualifier;        /* 'h', 'l', or 'L' for integer fields */
    for (str=buf ; *fmt ; ++fmt) 
    {
        if (*fmt != '%') 
        {                       
            *str++ = *fmt;            
            continue;
        }
        /* process flags */
        flags = 0;
        repeat:
            ++fmt;        /* this also skips first '%' */                         
            switch (*fmt) 
            {
                case '-': flags |= LEFT; goto repeat;
                case '+': flags |= PLUS; goto repeat;
                case ' ': flags |= SPACE; goto repeat;                      
                case '#': flags |= SPECIAL; goto repeat;
                case '0': flags |= ZEROPAD; goto repeat;
             }
        /* get field width */
        field_width = -1;
        if (is_digit(*fmt))
            field_width = skip_atoi(&fmt);
        else if (*fmt == '*') 
        {
            /* it's the next argument */
            field_width = va_arg(args, int);
            if (field_width < 0) 
            {
                field_width = -field_width;
                flags |= LEFT;
            }
        }
        /* get the precision */
        precision = -1;
        if (*fmt == '.') 
        {
            ++fmt;  
            if (is_digit(*fmt))
                precision = skip_atoi(&fmt);
            else if (*fmt == '*') 
            {
                /* it's the next argument */
                precision = va_arg(args, int);
            }
            if (precision < 0)
                precision = 0;
        }
        /* get the conversion qualifier */
        qualifier = -1;
        if (*fmt == 'h' || *fmt == 'l' || *fmt == 'L') 
        {
            qualifier = *fmt;
            ++fmt;
        }
        switch (*fmt) 
        {   
            case 'c':
            if (!(flags & LEFT))
                while (--field_width > 0)
                    *str++ = ' ';
            *str++ = (unsigned char) va_arg(args, int);
            while (--field_width > 0)
                *str++ = ' ';
            break;
            case 's':
            s = va_arg(args, char *);
            len = strlen(s);
            if (precision < 0)
                precision = len;
            else if (len > precision)
                len = precision;
            if (!(flags & LEFT))
                while (len < field_width--)
                    *str++ = ' ';
            for (i = 0; i < len; ++i)
                *str++ = *s++;
            while (len < field_width--)
                *str++ = ' ';
            break;
            case 'o':
            str = number(str, va_arg(args, unsigned long), 8,
                field_width, precision, flags);
            break;
                 case 'p':
                     if (field_width == -1) 
                    {
                         field_width = 8;
                         flags |= ZEROPAD;
                     }
                     str = number(str,
                         (unsigned long) va_arg(args, void *), 16,
                         field_width, precision, flags);
                     break;
                 case 'x':
                     flags |= SMALL;
                 case 'X':
                     str = number(str, va_arg(args, unsigned long), 16,
                         field_width, precision, flags);
                     break;
            case 'd':                                  
                 case 'i':
                     flags |= SIGN;
                 case 'u':
                     str = number(str, va_arg(args, unsigned long), 10,
                         field_width, precision, flags);
                     break;
                 case 'n':
                     ip = va_arg(args, int *);
                     *ip = (str - buf);
                     break;
                 default:
                     if (*fmt != '%')
                         *str++ = '%';
                     if (*fmt)
                         *str++ = *fmt;
                     else
                         --fmt;
                     break;
                 }
             }
             *str = '\0';
             return str-buf;
}

这个函数的作用是把后面的参数加到字符串里面然后输出字符串的长度。
再看看write函数(汇编代码):

write:
     mov eax, _NR_write
     mov ebx, [esp + 4]
     mov ecx, [esp + 8]
     int INT_VECTOR_SYS_CALL

int INT_VECTOR_SYS_CALL的作用是调用sys_call这个函数,看看这个函数的反汇编:

sys_call:
     call save
     push dword [p_proc_ready]
     sti    
     push ecx
     push ebx
     call [sys_call_table + eax * 4]
     add esp, 4 * 3     
     mov [esi + EAXREG - P_STACKBASE], eax    
     cli    
     ret

可以看出代码里面的call是访问字库模板并且获取每一个点的RGB信息最后放入到eax也就是输出返回的应该是显示vram的值,然后系统显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码转换成ASCII码,保存到系统的键盘缓冲区之中。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

本章我们就hello里面的函数对应unix的I/O来细致地分析了一下I/O对接口以及操作方法,这有助于我们以后在写函数的时候在标准I/O 库没有的时候我们可以编写自己的I/O函数。

结论

用计算机系统的语言,逐条总结hello所经历的过程。
1.hello.c产生在键盘鼠标等I/O设备下,通过文件的方式储存在主存里面。
2.预处理器将hello.c预处理成为文本文件hello.i
3.编译器将hello.i翻译成汇编语言hello.s
4.汇编器将hello.s汇编成可重定位二进制代码hello.o
5.链接器将外部文件和hello.o合体成为可执行二进制文件hello
6.shell通过execve创建了一个新进程并把hello完好无损地安排进去
7.hello在新进程中,shell帮hello把hello的想要的东西都配置好(创建新的内存区域,并创建一组新的代码、数据、堆和栈段。并安排好内容),就等着hello在此运行
8.hello在执行的过程中一样也会遇到异常和信号以及命令,hello都会认真执行
9.在工作的过程中hello总会遇见不懂的VA
10.hello运行完成,shell有始有终的帮hello进行回收,hello几次消失

附件

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

文件名称文件作用
hello.i预处理后的文本文件
hello.s编译后的汇编文件
hello.o汇编后的可重定位目标文件
hello链接后的可执行文件
hello.elfhello的ELF格式
helloo.txthello.o反汇编文件
hello.txthello反汇编文件

参考文献

为完成本次大作业你翻阅的书籍与网站等
[1] 林来兴. 空间控制技术[M]. 北京:中国宇航出版社,1992:25-42.
[2] 辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.
[3] 赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).
[4] 谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.
[5] KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.
[6] CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值