摘要
Hello,一个从诞生到谢幕的时间比昙花还短的程序。都说生命力越短的东西越凄美,于是我们便探寻了Hello的本质,利用gcc等工具,在linux系统下追寻着Hello的生命周期,没想到打开的是对系统的学习的大门——从它被程序猿创建开始,到在系统上运行,输出简单的消息,然后终止。这才明白,Hello的一生是多么高贵和坎坷啊!
关键词
Hello;生命周期;系统学习;
目录
第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简介
Hello程序的生命周期是从一个高级C语言程序开始的。在linux系统上,GCC编译器驱动程序读取源程序文件hello.c,通过执行预处理阶段、编译阶段、汇编阶段和链接阶段这四个阶段翻译成一个可执行目标文件hello。
为了在系统中运行该可执行文件,我们将其文件名输入到称为shell的命令行解释器中,shell通过调用一个专门的函数,即系统调用,来加载运行。系统调用将控制权传递给操作系统。操作系统保存shell进程的上下文,(fork)创建一个新的hello子进程及其上下文,然后将控制权传给新的hello子进程。此即P2P过程(From Program to Process)。
之后,shell为该子进程execve,mmap等一系列指令来加载可执行目标文件hello(exceve加载并运行时有四个步骤:删除已存在的区域结构、映射私有区域,映射共享区域,设置程序计数器PC,使之指向代码区域的入口点。)这些指令将hello目标文件中的代码和数据从磁盘复制到主存,一旦目标文件hello中的代码和数据被加载到主存,处理器就开始执行hello程序的main程序中的机器语言指令,这些指令将它的消息字符串字节从主存复制到寄存器文件,再从寄存器文件复制到显示设备,最终显示在屏幕上,hello程序在屏幕上输出它的消息,然后终止。hello进程终止后,操作系统恢复shell进程中的上下文,并将控制权传回给它,shell进程等待下一个命令行的输入。hello执行完后shell为其回收子进程,内核删除相关痕迹。此即020过程(From Zero-0 to Zero-0)
1.2 环境与工具
硬件环境:Intel(R)Core™i5-8250U CPU 1.60GHz 8G RAM X64 256G SSD + 1TGHD
软件环境:Ubuntu 18.04 LTS
开发与调试工具:gcc ld edb objdump HexEdit gedit readlef devC++
1.3 中间结果
文件名称 | 文件作用 |
---|---|
hello.c | c程序源代码 |
hello.i | hello.c预处理后生成的文本文件 |
hello.s | hello.i经编译器翻译后生成的文本文件 |
hello.o | hello.s经汇编器翻译后生成的可重定位目标文件 |
hello | hello.o和printf.o经链接器链接生成的可执行文件 |
hello.elf | hello.o文件的elf格式 |
hello.objdump | hello.o对应的反汇编文件 |
hello1.elf | hello的ELF格式 |
hello1.objdump | hello的反汇编文件 |
1.4 本章小结
本章主要对hello生命周期中的P2P和020进行了介绍,同时列出了实验需要使用的环境和工具,而且通过一个表格列出了hello执行过程中的中间结果,这一章是对整个过程的一个概览。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
①预处理的概念:
预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。结果就得到了另一个C程序,通常是以.i作为文件的扩展名。
②预处理的作用:
1、宏定义
2、文件包含
一个文件包含另一个文件的内容。
格式:#include“文件名” 或 #include<文件名>
E.g.hello.c中第一行的#include
3、条件编译
格式(1)
#ifdef +标识符 + 程序段1 + #else + 程序段2 + #endif
或
#ifdef + 程序段1 + #endif(当标识符已经定义时,程序段1才参加编译)
格式(2)
#ifndef + 标识符
#define + 标识1
程序段1
#endif
(如果标识符没有被定义,则重定义标识1,且执行程序段1)
格式(3)
#if + 表达式1
程序段1
#elif + 表达式2
程序段2
……
#elif + 表达式n
程序段n
#else
程序段n+1
#endif
(当表达式1成立,编译程序段1,不成立,查看表达式2…)
4、布局控制 #pragma
为编译程序提供非常规的控制流信号
2.2在Ubuntu下预处理的命令
命令:gcc -E hello.c -o hello.i
2.3 Hello的预处理结果解析
用vim打开hello.i,我们看到main函数出现在3110行。
图2.3.1 hello.i中Main函数
我们从开头开始看,发现它是对文件中包含的系统头文件的寻址和解析。
由预处理命令知,hello.c中包含的头文件和宏定义会被引入,如果头文件中还包含有其他文件,则进行递归寻址展开。与此同时,还引入了头文件中所有的typedef关键字,结构体类型,枚举类型等等。
图2.3.3 hello.i中间部分代码
2.4 本章小结
本章了解了预处理的概念和作用,并通过Ubuntu下对hello.c的预处理和hello.i的解析,对程序的预处理结果进行了深入而直观的认识。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
①编译的概念:
编译器是将高级语言程序解释成为计算机所需的详细机器语言指令集的程序。其工作过程主要分为五个阶段:词法分析,语法分析,语义检查和中间代码生成,代码优化,目标代码生成。
1、词法分析:
词法分析的任务是对由字符组成的单词进行处理,从左至右逐个字符地对源程序进行扫描,产生一个个的单词符号,把作为字符串的源程序改造成为单词符号串的中间程序。
2、语法分析:
编译程序的语法分析器以单词符号作为输入,分析单词符号串是否形成符合语法规则的语法单位,如表达式、赋值、循环等,最后看是否构成一个符合要求的程序,按该语言使用的语法规则分析检查每条语句是否有正确的逻辑结构,程序是最终的一个语法单位。
3、中间代码:
中间代码是源程序的一种内部表示,或称中间语言。中间代码的作用是使编译程序的结构在逻辑上更为简单明确。特别是可使目标代码的优化比较容易实现中间代码,即为中间语言程序,中间语言的复杂性介于源程序语言和机器语言之间。
4、代码优化:
代码优化是指对程序进行多种等价变换,使得从变换后的程序出发,能生成更有效的目标代码。
5、目标代码:
目标代码生成是编译的最后一个阶段。目标代码生成器把语法分析后或优化后的中间代码变换成目标代码。
②编译的作用:
编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。它主要进行词法分析和语法分析,分析过程中若发现有错误,编译程序会报告错误的性质和错误的发生地点,并且将错误所造成的影响限制在尽可能小的范围内,使得源程序的其余部分能继续被编译下去。
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.c -o hello.s
3.3 Hello的编译结果解析
3.3.1.汇编指令:
指令 | 含义 |
---|---|
.file | 声明源文件 |
.text | 代码段 |
.global | 声明全局变量 |
.type | 指定类型 |
.size | 声明大小 |
.align | 声明对指令或数据存放地址的对齐方式 |
.long | 声明long类型 |
.string | 声明string类型 |
.section、.radata | 只读数据 |
.data | 声明数据段 |
图3.3.1.hello.s中用到的汇编指令
3.3.2数据类型
Hello.s中用到的C数据类型有整数,字符串,数组。
(1)整数:
程序中涉及的整数有:int sleepsecs; int i; int argv; 立即数。
1、int sleepsecs.
sleepsecs在程序中被声明为全局变量,且已被赋值为2.5.
图3.3.2.1 sleepsecs在hello.c程序中的声明
由图3.3.2.2可知,编译器将其在.file代码段声明为全局变量;在.data代码段,设置对齐方式为4,设置类型为对象,设置大小为4字节,设置成long类型,值为2。(由于sleepsecs被定义为整型全局变量,所以对2.5进行向下取整;而将int声明为long类型可能是编译器的原因)
图3.3.2.2. hello.s中sleepsecs的声明
2、int i
编译器将局部变量存储在寄存器中或者栈中。
由图3.3.2.3知,hello.s将i存储在-4(%rbp)中,且占4字节大小
movl $0,-4(%rbp)
图3.3.2.3 hello.s中i变量
3、int argv
argv是mian函数的第一个参数。作为第一个参数传入,保存在栈空间-0x20(%rbp)中,占4字节大小。
movl %edi,-20(%rbp)
图3.3.2.4 hello.s中argv变量
4、立即数
如判断argc!=3、i<10的3、10等直接作为立即数存放在指令中,直接编码在汇编代码中。
(2)字符串
汇编语言将输出字符串作为全局变量保存。所以储存在.rodata的只读数据段中。此汇编文件中共有两个字符串,均为printf的参数:
1、原字符串1:“Usage: Hello 学号 姓名!\n ”,在hello.s中发现被编码为UTF-8格式,一个汉字以\345开头占3字节,一个!以\357开头占两个字节。
图3.3.2.5 hello.s中第一个printf的参数
2、原字符串2:“Hello %s %s\n”,此字符串为printf的格式化输出的字符串,正常保存,不必编码为UTF-8格式。
图3.3.2.6 hello.s中第二个printf的参数
(3)数组
此汇编文件中共有两次出现数组。
1、第一次是作为Main函数的第二个传入参数,同时是char的指针数组:
图3.3.2.7 hello.c中第一次出现的数组
图3.3.2.8 hello.s中数组元素的循环输出
由分析知,argv每个元素均为char*类型,大小为8B,且指向连续的栈空间,每次只需要通过起始地址argv和8B以及8B倍数相加即可指向数组中的其他元素。
由图3.3.2.9中hello.s汇编代码知,argv首地址为-32(%rbp),argv[1]作为printf的第一个参数,保存在寄存器%rsi中,地址为-24(%rbp),argv[2]作为printf的第二个参数,保存在寄存器%rdx中,地址为-16(%rbp)。
图3.3.2.9. hello.s中对应循环的汇编代码
3.3.3.类型转换
类型转换就是将数据从一种类型转换为另一种类型。C语言的类型转换有两种:自动类型转换和强制类型转换。但要注意,类型转换只是暂时的,都只是为了本次运算而进行的临时性转换,转换的结果也会保存到临时的存储空间,不会改变数据本来的类型或值。
3.3.3.1自动类型转换
自动类型转换就是编译器默默地,隐式地,偷偷地进行的数据类型转换,这种转换不需要程序员干预,会自动发生。
在赋值运算中,赋值号两边的数据类型不同时,需要把右边表达式的类型转换为左边变量的类型,这可能会导致程序失真,或者精度降低,所以说,自动类型转换不一定安全。
如hello.c中出现的自动类型转换,2.5是float类型的数据,需要先转换为int类型再赋值给sleepsecs.
int sleepsecs = 2.5
图3.3.3.1 hello.c中的自动类型转换
在不同类型混合运算中,编译器也会自动转换数据类型,将参与运算的所有数据先转换为同一类型的数据,然后再进行运算。转换按数据长度增加的方向进行,以保证数值不失真,或者精度不降低。转换规则如下:
图3.3.3.2 类型转换规则
3.3.3.2强制类型转换
根据需要,程序员在代码中明确提出要进行的类型转换,称为强制类型转换。使用强制类型转换时,程序员需要意识到其中潜在的风险。如,int到char*就是风险极高的一种转换,一般会导致程序崩溃。
3.3.4.汇编语言操作
3.3.4.1.数据传送
指令 | 效果描述 |
---|---|
movl S,D | D <-- S 传双字 |
movw S,D | D <-- S 传字 |
movb S,D | D <-- S 传字节 |
movsbl S,D | D <-- 符号扩展S 【符号位填充(字节->双字) 】 |
movzbl S,D | D <-- 零扩展S【零填充(字节->双字)】 |
pushl S | R[%esp] <-- R[%esp] – 4;M[R[%esp]] <-- S【压栈】 |
popl D | D <-- M[R[%esp]];R[%esp] <-- R[%esp] + 4;【出栈】 |
(1)hello.s中出现的mov类的指令。
图3.3.4.1 hello.s中的mov指令
(2)hello.s中出现的压栈指令。
pusq %rbp
图3.3.4.2 hello.s中的push指令
3.3.4.2算数和逻辑操作:
指令 | 效果描述 |
---|---|
leal S,D | D = &S movl地址,S地址入D,D仅能是寄存器 |
incl D | D++ 加1 |
decl D | D-- 减1 |
negl D | D = -D 取负 |
notl D | D = ~D 取反 |
addl S,D | D = D + S 加 |
subl S,D | D = D – S 减 |
imull S,D | D = D*S 乘 |
xorl S,D | D = D ^ S 异或 |
orl S,D | D = D |
andl S,D | D = D & S 与 |
sall k,D | D = D << k 左移 |
shll k,D | D = D << k 左移(同sall) |
sarl k,D | D = D >> k 算数右移 |
shrl k,D | D = D >> k 逻辑右移 |
(1)hello.s中出现的获取栈空间的指令
subq $32,%rsp
图3.3.4.3. hello.s中的subq指令
(2)hello.s中的计数器自增指令i++
addl $1,-4(%rbp)
图3.3.4.4. hello.s中出现的addl指令
3.3.5控制转移
(1)比较指令
指令 | 基于及描述 |
---|---|
cmpb S2,S1 | S1 – S2 比较字节,差关系 |
testb S2,S1 | S1 & S2 测试字节,与关系 |
cmpw S2,S1 | S1 – S2 比较字,差关系 |
testw S2,S1 | S1 & S2 测试字,与关系 |
cmpl S2,S1 | S1 – S2 比较双字,差关系 |
testl S2,S1 | S1 & S2 测试双字,与关系 |
1、hello.s中的比较指令1:i<10,实际上是计算了i-9,设置了条件码,为下一步的jle跳转做准备。
图3.3.5.1 hello.s中的cmpl指令1
2、hello.s中的比较指令2:argc!=3,实际上是计算了argc-3,设置了条件码,为下一步的je跳转做准备
图3.3.5.2 hello.s中的cmpl指令2
(2)跳转指令
3、hello.s中的跳转指令,若argv==3,跳转执行L2
图3.3.5.3 hello.s中的je指令
4、hello.s中的跳转指令,无条件跳转
图3.3.5.4 hello.s中的jmp指令
5、hello.s中的跳转指令,i<=9即跳转
图3.3.5.5 hello.s中的jle指令
6、hello.s中的转移控制指令,调用getchar()函数
图3.3.5.6 hello.s中的call指令1
7、hello.s中的转移控制指令,调用printf函数
图3.3.5.7 hello.s中的call指令2
8、hello.s中的转移控制指令,为返回准备栈
图3.3.5.8 hello.s中的leave指令
3.3.6.函数
计算机编译或运行时,使用某个函数来完成相关命令。在程序中通过对函数的调用来执行函数体,其过程与其他语言的子程序调相似。
Hello.c中涉及的函数操作有:
①int main(int argc ,char *argv[])
函数调用:main函数通过call才能被调用,call指令会先将下一条指令地址压栈,然后从内核中获取命令行参数和环境变量地址跳转main函数执行
函数退出:main函数有两种退出方式,一是当命令行参数不为3时,调用exit(1)退出;二是命令行参数为3执行完循环和getchar()之后return 0 退出。
函数分配内存:main函数通过pushq %rbp,movq %rsp,%rbp,subq $32,%rsp分配到栈空间。
函数释放内存:如果是exit(1)退出,则函数不会释放内存;如果是return 0退出,则通过leave指令和ret指令恢复栈空间。
②exit()函数
函数调用:设置%edi值为1,再通过call指令调用
③printf()函数
函数调用:第一次的printf通过call puts@PLT指令调用,因为它只将%rdi设置为了字符串首地址;第二次printf通过call printf@PLT指令调用,因为它不仅将%rdi设置为字符串的首地址,而且设置%rsi和%rdx分别为printf的输出参数。
④sleep()函数
将%edi设置为sleepsecs的值,通过call指令调用sleep函数,期间,若信号中断,则返回剩余时间,否则,返回0.
⑤getchar()函数
通过call指令调用getchar()函数,出错返回-1,否则返回输入转成的ASCII码
3.4 本章小结
本章全面而系统的叙述了编译器将预处理文件hello.i翻译为文本文件hello.s的操作和内容。通过汇编指令,数据类型,类型转换,控制转移和函数这几个方面对hello.s的相应部分结合与原理做出了具体而详细的解释。
(以下格式自行编排,编辑时删除)
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编的概念:把汇编语言翻译成机器语言的过程称为汇编。
汇编的作用:汇编器(as)将hello.s翻译成机器语言指令,并把这些指令打包成一种叫做可重定位目标程序的格式,将结果存在目标文件hello.o中。hello.o文件是一个二进制文件,它包含的是函数main的指令编码,如果在文本编辑器中打开hello.o文件,会看到一堆乱码。
4.2 在Ubuntu下汇编的命令
命令:gcc -c -o hello.o hello.s
图4.2.1生成hello.o
4.3 可重定位目标elf格式
命令:readelf -a hello.o>hello.elf
图4.3.1hello.o文件的ELF格式的生成
然后打开hello.elf,分析各组成部分:
(1)ELF Header
包含信息:Magic(生成该文件的系统的字的大小和字节顺序)、目标文件类型、数据、版本、系统架构、程序头起点、程序头大小,节头大小和数量等。
图4.3.2 ELF头
(2)Section Headers:
包含信息:各节的名称,大小,类型,全体大小,地址,旗标,链接,信息,对齐,偏移量。
图4.3.3 节头部表
(3)Relocation section
包含信息:Main函数调用的puts,exit,printf,sleep,getchar函数以及全局变量sleepsecs和.rodata节,除此之外,还有.radata字节的偏移量,信息,类型,符号值,符号名称,加数。
图4.3.4 重定位节
重定位项目重点分析:
重定位条目的结构:
Typedef struct{
long offset;//偏移量 8字节
Long type:32;//重定位类型,信息后的4个字节
Symbol:32;//在符号表中的偏移量,信息的前4个字节
Long addend;//计算重定位位置的辅助信息 8字节
};Elf64_Rela;
在这里出现的R X86_64PC32是重定位一个使用32位PC的相对地址引用;R X86_64_32是重定位一个使用32位PC的绝对地址的引用。
这里分别举exit()函数和.rodata分析,其他重定位条目情况相似:
①exit()相对地址引用
定义exit的重定位条目为r,则
{
r.offset = 0x25
r.symbol=exit
r.type = R_X86_64_PC32
r.addend = -0x4
}
链接过程中链接器通过公式:
refptr = s + r.offset
refaddr = ADDR(s) + r.offset
refptr = (unsigned)(ADDR(r.symbol)+r.addend-refaddr)
可以得出refptr。
②.rodata节的绝对地址引用
定义其重定位条目为r,则
{
r.offset = 0x16
r.symbol=.radata
r.type = R_X86_64_PC32
r.addend = 0x0
}
链接过程中链接器指向.radata+0x0的位置,通过公式:
refptr = s + r.offset
refptr = (unsigned)(ADDR(r.symbol)+r.addend)
可以得出refptr。
(4)Symbol table
包含信息:程序中定义和引用函数和全局变量的信息,并声明重定位需要引用的符号。
图4.3.5符号表
4.4 Hello.o的结果解析
命令:objdump -d -r hello.o > hello.objdump
图4.4.1生成hello.o的反汇编文件
查看hello.objdump同时与hello.s进行比较:
主要异同的比较从以下两个方面叙述:
①分支转移:
hello.objdump包含的是由操作数和操作码构成的机器语言。因为函数内部跳转不必通过链接确定,所以跳转指令中的地址为已经确定的实际地址。
hello.s主要是使用像.L0,.L1这样的符号完成内部跳转和函数调用。
图4.4.2 hello.objdump部分代码
②函数调用:
hello.objdump文件中,Call后均有4个字节的0占位符,指向的是下一条地址的位置,因为其需要通过链接重定位寻址
Hello.s文件中的函数则是直接通过call+函数名调用,无需寻址。
4.4.3 hello.objdump部分代码(2)
4.5 本章小结
本章介绍了从hello.o到hello.s的汇编过程。通过用objdump查看其反汇编代码与hello.s比较,了解了机器语言的构成,及其与汇编语言的映射关系,特别是机器语言中的操作数与汇编语言不一致,还有分支转移和函数调用等的不同。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
链接的概念:链接是将各种代码和数据片段收集并组合成为一个单一的文件的过程。这个文件可被加载(复制)到内存并执行。
链接的作用:可以将源文件分解成更小更好管理的模块,可以独立修改编译这些模块,当改变模块中的一个时,只需简单地重新编译它,并重新链接而不必修改编译其他文件。
5.2 在Ubuntu下链接的命令
命令:gcc -m64 -no-pie -fno-PIC hello.c -o hello
图5.2 链接生成可执行文件
5.3 可执行目标文件hello的格式
命令:readelf -a hello > hello1.elf
图5.3.1生成hello的ELF格式
打开hello1.elf,分析各组成部分:
(1)ELF Header:
节头部表包含各段的基本信息
图5.3.2 可执行目标文件ELF
(2)Section Headers
section headers对所有节的信息进行了声明,包括大小size和偏移量offset,根据这些信息就可以用HexEdit确定各段的起始位置和大小。
图5.3.3 hello1.elf中的section headers
5.4 hello的虚拟地址空间
edb加载hello 可以从Data Dump中查看虚拟地址空间,程序的虚拟地址空间为0x0000000000400000-0x0000000000401000.
图5.4.1虚拟地址空间
首先查看hello1.elf的程序头部分:
图5.4.2.hello1.elf的程序头
由程序头可知:
(1)PHDR部分:具有读权限。
起始位置:0x400000偏移0x40字节处,大小为0x1f8字节
(2)INTERP部分:具有读权限。
起始位置:0x400000偏移0x238字节,大小为0x01c字节
(还记录了动态链接器的位置位于:/lib64/ld-linux-x86-64.so.2)
(3)LOAD部分:代码段,有读/执行访问权限
起始位置:0x400000,大小为0x890字节
(4)LOAD部分:数据段,具有读/写权限。
起始位置:0x600e10,大小0x248字节
(5)NOTE部分:具有读权限。
起始位置:0x400000偏移0x254字节,大小为0x44字节
5.5 链接的重定位过程分析
命令:objdump -d -r hello>hello1.objdump
图5.5.1hello反汇编文件的生成
(1)hello与hello.o的异同
文件内容:
hello反汇编文件中包含Disassembly of section .int .plt .text .fini
hello.o反汇编文件中包含Disassembly of .text
对hello中的不同段做出说明:
.init 初始化程序代码
.plt 动态链接过程链接表
.fini 程序终止执行代码
函数调用:
hello.o中,call地址后为4个0字节的占位符;
hello的生成过程中调用了动态链接共享库,链接器解析重定向条目时,动态链接库中的函数已经加入到了PLT中,链接器计算.text节与.plt节相对距离,且将对动态链接库中的函数调用值改为PLT中相应函数与下一条指令的相对地址。
5.6 hello的执行流程
5.7 Hello的动态链接分析
动态链接器使用PLT和GOT实现函数动态链接。
在dl_init调用之前,GOT存放的是PLT中函数调用的下一条指令。
图5.7.1.GOT表位置
在dl_init调用之后,GOT指向重定位表用来确定调用函数地址。
5.8 本章小结
本章了解了链接的概念和作用,同时分析了可执行文件hello的ELF格式和虚拟地址空间。通过重定位过程和对执行流程的分析,使用edb对动态链接过程的分析,对链接和重定位的过程进行了深入的理解。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:进程就是一个执行中程序的实例。
进程的作用:进程给应用程序提供了两个关键的抽象。
6.2 简述壳Shell-bash的作用与处理流程
Shell的作用:
Shell是UNIX提供的供用户使用的界面,是一个命令解释程序, Shell为用户提供了输入命令和参数并可得到命令执行结果的环境。
Shell的处理流程:
(1)读取输入的命令行
(2)解析引用并分割命令行为
(3)结束后进行相关处理,如进行扩展,截断文件等。
6.3 Hello的fork进程创建过程
在命令行键入./hello,命令行会对该命令进行解析,发现不是内置命令,则判断为可执行文件,然后终端程序会调用fork()函数创建一个新的子进程,新创建的子进程几乎但不完全与父进程相同。子进程得到和父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码段和数据段、堆,共享库和用户栈。子进程还获得与父进程描述相同的副本,即当父进程调用fork()时,子进程可以读写父进程中打开的任何文件。父进程和子进程的区别在于他们有不同的pid.
6.4 Hello的execve过程
系统为hello fork子进程后,子进程会调用execve函数加载运行可执行文件hello。execve函数调用的加载器有四个步骤:删除已存在的用户区域,映射私有区域,映射共享区域,设置程序计数器PC。
6.5 Hello的进程执行
以hello和sleep的进程调度过程为例:
当hello在执行时,hello处于用户模式,若此时被sleep中断,则进行上下文切换,进入内核模式,hello进入等待队列。(上下文切换是在内核调度器中完成的,当内核调度新的进程运行时,首先会保存hello进程的上下文,恢复新恢复进程的上下文,将控制转移。)当2.5secs完成后,定时器发送中断信号,内核状态进入中断处理,hello又进入了运行状态,继续自己时间片上的逻辑控制流。
6.6 hello的异常与信号处理
hello执行过程中出现的异常种类可能有:中断,陷阱,故障,终止。
信号允许进程和内核中断其他进程。信号提供一种机制,通知用户进程发生了这些异常。
(在程序运行时键入ctrl+z,会导致内核发送SIGTSTP信号给hello,同时发送SIGSHLD信号给父进程)
(执行ps显示当前进程数量和内容,其中hello是被暂停的进程,PID为3201)
图6.6.3 键入Ctrl+z 后执行jobs
(键入ctrl+z后执行jobs显示当前暂停过程)
(键入ctrl+z后执行pstree显示当前进程树)
图6.6.5 键入Ctrl+z后执行fg
图6.6.6. 键入ctrl+z后执行kill
(执行kill命令杀死进程)
(程序执行过程中键入ctrl+c终止hello进程)
6.7本章小结
本章介绍了进程的定义和作用,Shell的处理流程,如何调用fork()创建子进程,如何调用execve函数执行可执行程序以及hello进程执行时的异常处理情况。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:由段地址和偏移地址构成。
线性地址:段地址+偏移地址
虚拟地址:段式管理中:逻辑地址->线性地址=虚拟地址
物理地址:真实的物理内存地址。
以hello中的puts为例:
Puts输出字符串中的逻辑地址中的偏移地址,需要经过段地址到线性地址的转换,变为虚拟地址,再通过MMU转换为物理地址,找到对应物理内存。
7.2 Intel逻辑地址到线性地址的变换-段式管理
机器语言指令中出现的内存地址,都是逻辑地址,需要转换成线性地址,再经过MMU(CPU中的内存管理单元)转换成物理地址才能够被访问到。
图7.2(转)逻辑地址到线性地址
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址通过页表查找来对应物理地址。
分页的基本原理是把内存划分成大小固定的若干单元,每个单元称为一页,每页包含4k字节的地址空间。每一页的起始地址都是4k字节对齐的。为了能转换成物理地址,需要给CPU提供当前任务的线性地址转物理地址的查找表,即页表。为了实现每个任务的平坦的虚拟内存,每个任务都有自己的页目录表和页表。
为了节约页表占用的内存空间,x86将线性地址通过页目录表和页表两级查找转换成物理地址。
图7.3(转)线性地址转物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
TLB是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块。用于组选择和行匹配的索引和标记字段是从虚拟地址的虚拟页号中提取出来的。TLB中所有地址翻译步骤都是在MMU上执行的。
具体步骤为:
使用四级页表的地址翻译步骤:
如果TLB未命中,MMU会向页表中查询,CR3确定第一级页表的起始地址,VPN1确定第一级页表的偏移量,查询出PTE,如果在物理内存中,且权限符合,则确定第二级页表的起始地址,以此类推,最终在第四级页表中查询到PPN与VPO组合成PA。
7.5 三级Cache支持下的物理内存访问
以L1 cache为例(L2,L3类似)
通过获得的物理地址PA,使用CL进行组索引,每组分别匹配CT,如果匹配成功且块的valid标志位为1,则命中(hit),根据CO取出数据返回。如果没有匹配成功或者valid标志位不为1,则不命中(Miss),向下一级缓存查询(L2->L3->主存).查询到数据后,组内有空闲块,则直接存储,否则进行LFU替换。
7.6 hello进程fork时的内存映射
当shell进程调用fork函数时,内核会为hello创建子进程,同时创建各种数据结构并分配唯一的PID。为了给hello创建虚拟内存,内核创建了当前进程的mm_struct、区域结构和样表的原样副本,将两个进程的页面都标记为只读,并将两个进程中的每个区域结构标记为写时复制。
7.7 hello进程execve时的内存映射
execve函数调用内核区域的启动加载器加载运行可执行目标文件hello。
加载运行hello有以下四个步骤:
①删除已存在的用户区域
②映射私有区域。为新进程的代码、数据、bss和栈区域创建新的区域结构。
③映射共享区域。hello程序和共享对象libc.so链接。
④设置程序计数器PC。
7.8 缺页故障与缺页中断处理
缺页故障:当指令引用一个虚拟地址,在MMU中查找页表时发现与该地址相对应的物理地址不在内存中,会发生缺页故障。
缺页中断处理:系统内核中的缺页处理程序选择一个牺牲页面,如果这个页面被修改过,则进行更新页表。
7.9动态存储分配管理
printf会调用malloc,而动态存储分配管理的方法和策略简要叙述如下:
动态内存分配器维护着进程的一个虚拟内存区域,称为堆;分配器将堆视为一组不同大小的块(blocks)的集合来维护,每个块要么是已分配的,要么是空闲的。
分配器的类型有隐式分配器(应用检测到已分配块不再被程序所使用,就释放这个块)和显式分配器(要求应用显式地释放任何已分配)。
①带边界标签的隐式空闲链表分配器原理:
在空闲块的“底部”标记 “大小/已分配”;可以反查 “链表”,但需要额外的空间。具体原理可用下图体现:
②显示空间链表的基本原理:
显式空闲链表 在空闲块中使用指针连接空闲块;
维护空闲块链表, 而不是所有块
▪ “下一个” 空闲块可以在任何地方
▪ 因此需要存储前/后指针,而不仅仅是大小(size)
▪ 还需要边界标记,用于块合并
▪ 幸运的是,只需跟踪空闲块,因此可以使用有效载荷区域
图7.9.2显式空闲链表
7.10本章小结
本章主要介绍了hello的存储器地址空间、intel的段式管理、hello的页式管理,以及VA到PA的变换、物理内存访问和hello进程fork时的内存映射、execve时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
所有的IO设备都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单低级的应用接口,称为Unix I/O。
8.2 简述Unix IO接口及其函数
①Unix IO接口:
将设备映射为文件的方式,允许Unix内核引出一个简单、低级的应用接口
②Unix I/O函数:
int open(char* filename,int flags,mode_t mode) //返回:若成功则为新文件描述符,若出错为-1
通过调用open函数来打开一个存在的文件或是创建一个新文件的。
int close(fd),fd是需要关闭的文件的描述符,close返回操作结果。
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.
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;
}
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’: //只处理%x一种情况
itoa(tmp, ((int)p_next_arg)); //将输入参数值转化为字符串保存在tmp
strcpy(p, tmp); //将tmp字符串复制到p处
p_next_arg += 4; //下一个参数值地址
p += strlen(tmp); //放下一个参数值的地址
break;
case ‘s’:
break;
Default:
break;
}
}
return (p - buf); //返回最后生成的字符串的长度
}
作用是产生格式化输出。
write函数:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL;
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_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
syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。显示驱动子程序将通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中。显示芯片会按照一定的刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)
8.4 getchar的实现分析
Getchar()函数函数体:
int getchar(void)
{
static char buf[BUFSIZ];
static char *bb = buf;
static int n = 0;
if(n == 0)
{
n = read(0, buf, BUFSIZ);
bb = buf;
}
return(–n >= 0)?(unsigned char) *bb++ : EOF;
}
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章介绍了Linux的IO设备管理方法、Unix IO接口及其函数,重点分析了printf函数和getchar函数。
(第8章1分)
结论
HELLO的生命历程:
1、编写hello代码,通过IDE将代码键入hello.c
2、预处理,hello.c经过预处理器处理得到文本文件hello.i
3、编译,hello.i编译成汇编文件hello.s
4汇编,hello.s经汇编器翻译成机器语言指令,打包成为可重定位目标文件hello.o
5、链接,hello.o与可重定位目标文件和动态链接库链接成可执行目标程序hello
6、运行:在shell中输入./hello 1170300717 Huangchunjiao运行hello,shell进程调用fork为hello创建子进程
7、运行程序:子进程调用execve,execve调用启动加载器,映射虚拟内存,进入程序入口后程序载入物理内存,进入 main函数。
8、执行指令:CPU为进程分配时间片,执行的控制逻辑流
9、访问内存:MMU将虚拟内存地址通过页表映射成物理地址。
10、动态申请内存:printf调用malloc向动态内存分配器申请堆中的内存。
11、信号:如果运行中键入ctr-c ctr-z则调用shell的信号处理函数分别停止、挂起。
12、结束:shell父进程回收子进程,内核清理所有痕迹。
(结论0分,缺失 -1分,根据内容酌情加分)
感悟:计算机系统是一门非常实用的课程,现在的我们也只是雾里看花,但我们也知道,终有一天我们会真正了解其精髓。现在,通过这个hello的自白的大作业,也真真切切感受到hello的奇妙,真的是一个简简单单的程序打开系统学习的大门,回头看才知晓自己当初是多么无知。
“从前看山是山,看水是水;然后看山不是山,看水不是水;而后看山又是山,看水又是水。”或许这句话中蕴含的哲理恰能说明每一个学习计算机的同学的心情!
(结论0分,缺失 -1分,根据内容酌情加分)
(附件0分,缺失 -1分)
参考文献
[1].https://baike.baidu.com/item/预处理命令/10204389 百度百科预处理命令.
[2].https://www.cnblogs.com/lxgeek/archive/2011/01/01/1923738.html汇编指令
[3].https://blog.csdn.net/erazy0/article/details/6457626
[4].https://www.cnblogs.com/whc-uestc/p/4365507.html
[5].https://baike.baidu.com/item/编译
[6].http://c.biancheng.net/cpp/html/24.htmlC语言自动类型转换
[7].Csapp《深入理解计算机系统》
(参考文献0分,缺失 -1分)