哈工大2023ICS大作业陈诉——程序人生-Hello‘s P2P
摘 要本文旨在研究hello.c这一简单c语言文件在Linux体系下的整个生命周期,以其原始程序开始,依次深入研究了编译、链接、加载、运行、终止、回收的过程,从而了解hello.c文件的“一生”。本文以hello.c文件为研究对象,结合《CS:APP》中的内容,在Ubuntu体系下对hello程序的整个生命周期进行了研究,通过对hello.c程序的深入研究,把计算机体系整个的体系串联在一起.
关键词:计算机体系;CSAPP;ubuntu;hello;
目 录
第1章 概述... - 4 -
1.1 Hello简介... - 4 -
1.2 环境与工具... - 4 -
1.3 中心效果... - 5 -
1.4 本章小结... - 5 -
第2章 预处置惩罚... - 6 -
2.1 预处置惩罚的概念与作用... - 6 -
2.2在Ubuntu下预处置惩罚的命令... - 6 -
2.3 Hello的预处置惩罚效果解析... - 7 -
2.4 本章小结... - 7 -
第3章 编译... - 8 -
3.1 编译的概念与作用... - 8 -
3.2 在Ubuntu下编译的命令... - 8 -
3.3 Hello的编译效果解析... - 8 -
3.4 本章小结... - 16 -
第4章 汇编... - 18 -
4.1 汇编的概念与作用... - 18 -
4.2 在Ubuntu下汇编的命令... - 18 -
4.3 可重定位目标elf格式... - 18 -
4.4 Hello.o的效果解析... - 22 -
4.5 本章小结... - 24 -
第5章 链接... - 25 -
5.1 链接的概念与作用... - 25 -
5.2 在Ubuntu下链接的命令... - 25 -
5.3 可执行目标文件hello的格式... - 26 -
5.4 hello的虚拟地址空间... - 30 -
5.5 链接的重定位过程分析... - 32 -
5.6 hello的执行流程... - 34 -
5.7 Hello的动态链接分析... - 34 -
5.8 本章小结... - 35 -
第6章 hello进程管理... - 36 -
6.1进程的概念与作用... - 36 -
6.2 简述壳Shell-bash的作用与处置惩罚流程... - 36 -
6.3 Hello的fork进程创建过程... - 36 -
6.4 Hello的execve过程... - 37 -
6.5 Hello的进程执行... - 37 -
6.6 hello的非常与信号处置惩罚... - 38 -
6.7本章小结... - 42 -
第7章 hello的存储管理... - 43 -
7.1 hello的存储器地址空间... - 43 -
7.2 Intel逻辑地址到线性地址的变换-段式管理... - 43 -
7.3 Hello的线性地址到物理地址的变换-页式管理... - 44 -
7.4 TLB与四级页表支持下的VA到PA的变换... - 45 -
7.5 三级Cache支持下的物理内存访问... - 46 -
7.6 hello进程fork时的内存映射... - 47 -
7.7 hello进程execve时的内存映射... - 47 -
7.8 缺页故障与缺页中断处置惩罚... - 47 -
7.9动态存储分配管理... - 48 -
7.10本章小结... - 49 -
第8章 hello的IO管理... - 50 -
8.1 Linux的IO设备管理方法... - 50 -
8.2 简述Unix IO接口及其函数... - 50 -
8.3 printf的实现分析... - 51 -
8.4 getchar的实现分析... - 54 -
8.5本章小结... - 54 -
结论... - 55 -
附件... - 56 -
参考文献... - 57 -
第1章 概述
1.1 Hello简介
首先对Hello的P2P,020进行简述。
Hello的P2P是指hello.c文件从可执行程序(Program)变为运行时进程(Process)的过程。在Linux体系下,hello.c 文件依次颠末cpp(C Pre-Processor,C预处置惩罚器)预处置惩罚、ccl(C Compiler,C编译器) 编译、as (Assembler,汇编器)汇编、ld (Linker,链接器)链接最终成为可执行目标程序hello(在Linux下该文件无固定后缀)。打开shell,输入命令./hello后,shell 通过fork产生子进程,hello 便从可执行程序(Program)酿成为进程(Process)。
Hello的020是指hello.c文件“From zero-0 to zero-0”,初始时内存中并无hello文件的相干内容,这便是“From 0”。通过在Shell下调用execve函数,体系会将hello文件载入内存,执行相干代码,当程序运行结束后, hello进程被回收,并由内核删除hello相干数据,这即为“to 0”。
1.2 环境与工具
硬件:X64 CPU;2GHz;2G RAM;256GHD Disk 以上
软件:Windows10 64位; VMware Workstation 16 Pro; Ubuntu 22.10 64位
调试工具:Visual Studio 2019 64-bit;
gedit,gcc,readelf, objdump, hexedit, edb
1.3 中心效果
表 1 中心效果
文件名
功能
hello.i
预处置惩罚后得到的文本文件
hello.s
编译后得到的汇编语言文件
hello.o
汇编后得到的可重定位目标文件
hello.elf
用readelf读取hello.o得到的ELF格式信息
hello.asm
反汇编hello.o得到的反汇编文件
hello2.elf
由hello可执行文件天生的.elf文件
hello2.asm
反汇编hello可执行文件得到的反汇编文件
1.4 本章小结
本章简要介绍了hello 的P2P,020的具体含义,同时列出了研究时采用的具体软硬件环境和中心效果。
第2章 预处置惩罚
2.1 预处置惩罚的概念与作用
[*]预处置惩罚的概念
CPP作为预处置惩罚器可以或许根据以“#”开头的命令行(比方头文件,宏界说等), 将源程序引用的全部库展开合并,从而将它修改称为一个文本文件。
[*]预处置惩罚的作用
1)将源文件中以”#include”格式包罗的文件复制到编译的源文件中。
2)用实际值更换用”#define”宏界说的字符串。
3)根据”#if”后面的条件决定必要编译的程序代码。称”#if”为条件编译指令, 预处置惩罚能解读的这种指令包罗#ifdef、#ifndef、#else、#esif、#endif等。
4)处置惩罚特殊符号,比方LINE解读为行号,FILE解读为当前处置惩罚的文件等。
2.2在Ubuntu下预处置惩罚的命令
在Ubuntu体系下,进行预处置惩罚的命令为:
cpp hello.c > hello.i
图 1 预处置惩罚过程
2.3 Hello的预处置惩罚效果解析
在Linux下打开hello.i文件,可以发现hello.i程序已经拓展为3110行,行数比起hello.c文件大幅增长。此中, hello.c中的main函数相干代码在hello.i程序中对应着3094行到3110行。
图 2 预处置惩罚效果部分展示
主要是对源文件中的宏界说等进行了宏展开,比方头文件的内容被写进hello.i文件中(函数声明、布局体界说、界说变量、宏界说等内容)。此外,代码中的#define命令会对界说的字符串作相应的更换。
2.4 本章小结
本章介绍了预处置惩罚相干概念以及处置惩罚具体内容,比方更换宏界说符号、展开合并头文件内容、解读条件编译指令和特殊符号等。
第3章 编译
[*]
[*] 编译的概念与作用
[*]编译的概念
编译是指C编译器ccl通过词法分析和语法分析,将正当指令翻译成等价汇编代码的过程。通过编译过程,编译器将文本文件 hello.i 翻译成汇编语言文件 hello.s,在hello.s中,以文本的情势描述了一条条低级机器语言指令。
[*]编译的作用
将文本文件翻译成汇编语言程序,为后续将其转化为二进制机器码做准备。
这里的编译是指从 .i 到 .s 即预处置惩罚后的文件到天生汇编语言程序
3.2 在Ubuntu下编译的命令
参考PPT,在Ubuntu体系下,进行预处置惩罚的命令为:
gcc -m64 -no-pie -fno-PIC -S hello.i -o hello.s
运行截图如下:
图 3 编译过程
3.3 Hello的编译效果解析
[*]文件布局分析
对hello.s文件团体布局分析如下:
表 2 hello.s文件布局
内容
含义
.file
源文件
.text
代码段
.global
全局变量
.data
存放已经初始化的全局和静态C 变量
.section .rodata
存放只读变量
.align
对齐方式
.type
表示是函数范例/对象范例
.size
表示大小
.long .string
表示是long范例/string范例
[*]数据范例
在hello.s中,涉及的数据范例包罗以下三种:整数,字符串,数组。下面对每种数据范例依次进行分析。
[*]整数
在hello.s中,涉及的整数有:
[*]int sleepsecs
查看C语言文件可知,sleepsecs为int型全局变量,已被初始化赋值2.5。
颠末编译阶段得到的hello.s文件中,编译器在.text段中将sleepsecs声明为全局变量,在.type段声明其为object范例,在.size段声明其长度为4,设置其值为2。具体环境如下:
图 4 sleepsecs的环境
[*]int i
编译器将局部变量存储在寄存器大概栈空间中。i作为函数内部的局部变量,并不占用文件实际节的空间,只存在于运行时栈中。对于i的操作就是直接对寄存器或栈进行操作。
在hello.s中我们可以看出,i占据了4字节的地址空间:
[*]int argc
argc是main函数的参数之一,64位编译下,由寄存器传入,进而生存在堆栈中。
[*]立刻数3
立刻数3在汇编语句中直接以$3的情势出现
[*]字符串
程序中生存了两个字符串,分别为:
图 5 字符串的环境
两者均为字符串常量,储存在.text数据段中。\XXX为UTF-8编码,一个汉字对应三个字节。
[*]数组
程序中涉及的数组为char *argv[],即函数的第二个参数。在hello.s中,其首地址生存在栈中。访问时,通过寄存器寻址的方式访问。
图 6 数组的环境
[*]赋值操作
[*]int sleepsecs=2.5
在C语言源程序中包罗一个隐式范例转换:将2.5赋值给一个int范例,效果为2。在hello.s中,直接在.data节中将sleepsecs声明为值2的long范例数据。
[*]int i
对局部变量的赋值在汇编代码中通过mov指令完成。具体使用哪条mov指令由数据的大小决定,如图所示:
表 3 mov指令的后缀
后缀
b
w
l
q
大小(字节)
1
2
3
4
[*]范例转换
在C语言源程序中包罗一个隐式范例转换:
将浮点型的2.5赋值给int范例,值向零舍入, 2.5 舍入为 2。
[*]算数操作
汇编语言中,算数操作的指令包罗:
表格 4 算数操作指令
指令
效果
leaq s,d
d=&s
inc d
d+=1
dec d
d-=1
neg d
d=-d
add s,d
d=d+s
sub s,d
d=d-s
imulq s
r[%rdx]:r[%rax]=s*r[%rax](有符号)
mulq s
r[%rdx]:r[%rax]=s*r[%rax](无符号)
idivq s
r[%rdx]=r[%rdx]:r[%rax] mod s(有符号) r[%rax]=r[%rdx]:r[%rax] div s
divq s
r[%rdx]=r[%rdx]:r[%rax] mod s(无符号) r[%rax]=r[%rdx]:r[%rax] div s
在hello.s中,具体涉及的算数操作包罗:
[*]subq $32, %rsp:开发栈帧
[*]addq $16, %rax:修改地址偏移量
[*]addl $1, -4(%rbp):实现i++的操作
图 7 hello.s 中的算数操作
[*]关系操作
在hello.s中,具体涉及的关系操作包罗:
[*]argc != 3:
检查argc是否不等于3。在hello.s中,使用cmpl $3,-20(%rbp),根据判定的效果将信息送入标志寄存器中,然后进行分支跳转。
图 8 检查 argc != 3
[*]i < 10:
检查i是否小于10。
图 9 检查i<10
[*]数组操作
如前述3.3.2所述,hello.s中存在数组char *argv[],对其的访问操作通过寄存器寻址方式实现。
[*]控制转移
程序中控制转移的具体体现有两处:
[*]if(argc!=3):
当argc不等于3时,执行函数体内部的代码。在hello.s中,使用cmpl $3,-20(%rbp),比较 argc与3是否相等,若相等,则跳转至.L2,不执行后续部分内容;若不等则继续执行函数体内部对应的汇编代码。
图 10 控制转移
[*]for(i=0;i<10;i++):
当i < 10时进行循环,每次循环i++。在hello.s中,使用cmpl $9,-4 (%rbp),比较 i与9是否相等,在i<=9时继续循环,进入.L4,i>9时跳出循环。
图 11 循环的环境
[*]函数操作
hello.s中,程序入口处,调用了main 函数,其在hello.s中标注为@function函数范例。之后又调用 puts,printf,sleep,exit,getchar 函数,对函数的调用都通过call指令进行。
图 12 call指令的环境
3.4 本章小结
本章具体描述了编译阶段中GCC作为编译器怎样处置惩罚数据和操作,以及C语言中各种数据范例和函数操作,及其对应的汇编代码。通过了解编译器编译机制,我们能更好地处置惩罚将汇编代码翻译成C语言的问题。
第4章 汇编
4.1 汇编的概念与作用
[*]汇编的概念
汇编器(as)将汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将效果生存在.o目标文件中。.o是一种二进制文件,它包罗程序的机器指令编码。
[*]汇编的作用
天生可重定位文件.o,使得差别程序之间可以链接。同时解读汇编语言的助记 符,将汇编文件转化为机器指令。
4.2 在Ubuntu下汇编的命令
在Ubuntu下汇编命令:
gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o
汇编过程如下:
图 13 汇编命令
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特殊是重定位项目分析。
在shell中输入readelf -a hello.o > hello.elf 指令获得 hello.o 文件的 ELF 格式:
图 14 天生ELF文件
其布局分析如下:
[*]ELF 头(ELF Header):
以 16字节序列 Magic 开始,其描述了天生该文件的体系的字的大小和字节顺序,ELF 头剩下的部分包罗帮助链接器语法分析息争释目标文件的信息,此中包罗 ELF 头大小、目标文件范例、机器范例、节头部表的文件偏移,以及节头部表中条目标大小和数目等相干信息。
图 15 ELF头的环境
[*]节头:
包罗了文件中出现的各个节的意义,包罗节的范例、位置和大小等信息。
图 16 节头的环境
[*]重定位节.rela.text
一个.text 节中位置的列表,包罗.text 节中必要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,必要修改这些位置。
在这里,8 条重定位信息分别是对.L0(第一个 printf 中的字符串)、puts 函数、exit 函数、.L1(第二个 printf 中的字符串)、printf 函数、sleepsecs、sleep 函数、getchar 函数进行重定位声明。
.rela.text节包罗如下信息:
表 6 .rela.text节包罗的信息
偏移量
代表必要进行重定向的代码在.text或.data节中的偏移位置
信息
包罗symbol和type两部分,此中symbol占前半部分,type占后半部分,symbol代表重定位到的目标在.symtab中的偏移量,type代表重定位的范例
范例
重定位到的目标的范例
加数
计算重定位位置的辅助信息
图 17 .rela.text节
[*]重定位节.rela.eh_frame
图 18 .rela.eh_frame节
[*]符号表Symbol table
符号表中生存着定位、重定位程序中符号界说和引用的信息,全部重定位必要引用的符号都在此中声明。
图 19 符号表的环境
4.4 Hello.o的效果解析
用objdump -d -r hello.o > hello.asm 分析hello.o的反汇编,并与hello.s文件进行对照分析。
图 20 天生hello.asm文件
对比hello.asm与hello.s可知,两者在如下地方存在差异:
[*]分支转移:
在hello.s中,跳转指令的目标地址直接记为段名称,而在反汇编得到的hello.asm中,跳转的目标为具体的地址,在机器代码中体现为目标指令地址与当前指令下一条指令的地址之差。
图 21 分支转移
[*]函数调用:
在hello.s文件中,call后直接跟着函数名称,而在hello.asm中,call 的目标地址是当前指令的下一条指令。这是由于 hello.c 中调用的函数都是共享库中的函数,最终必要通过动态链接器作用才华确定函数的运行时执行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,将其 call 指令后的相对地址设置为全0(此时,目标地址正是下一条指令),然后在.rela.text 节中为其添加重定位条目,等待静态链接进一步确定。
图 22 函数调用
[*]全局变量访问:
在hello.s中,使用段名称+%rip访问 rodata(printf 中的字符串),而在反汇编得到的hello.asm中,使用 0+%rip进行访问。其缘故原由与函数调用类似,rodata 中数据地址在运行时才华确定,故访问时也必要重定位。在汇编成为机器语言时,将操作数设置为全 0 并添加相应的重定位条目。
图 23 全局变量访问
4.5 本章小结
本章对hello.s进行了汇编天生.o可重定位文件,并解析了它所蕴含的ELF头、节头部表、符号表和重定位信息。我们还阐释了汇编语言和反汇编代码的联系与差别,让我们对汇编语言到机器语言的处置惩罚过程和映射关系有了更深入的了解。
第5章 链接
5.1 链接的概念与作用
[*]链接的概念
链接是指通过链接器(Linker),将程序编码与数据块网络并整理成为一个单一文件,天生完全链接的可执行的目标文件(windows体系下为.exe文件,Linux体系下一样寻常省略后缀名)的过程。
[*]链接的作用
提供了一种模块化的方式,可以将程序编写为一个较小的源文件的集合,且实现了分开编译更改源文件,从而镌汰团体文件的复杂度与大小,增长容错性,也方便对某一模块进行针对性修改。
注意:这儿的链接是指从 hello.o 到hello天生过程。
5.2 在Ubuntu下链接的命令
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/7/crtbegin.o /usr/lib/gcc/x86_64-linux-gnu/7/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o hello.o -lc -z relro -o hello
链接过程如下:
图 24 链接命令
5.3 可执行目标文件hello的格式
在Shell中输入命令 readelf -a hello > hello2.elf 天生 hello 程序的 ELF 格式文件,生存为hello2.elf(与第四章中的elf文件作区分):
图 25 天生ELF文件
打开hello2.elf,分析hello的ELF格式如下:
[*]ELF 头(ELF Header)
hello2.elf中的ELF头与hello.elf中的ELF头包罗的信息种类基本相同,以 描述了天生该文件的体系的字的大小和字节顺序的16字节序列 Magic 开始,剩下的部分包罗帮助链接器语法分析息争释目标文件的信息。与hello.elf相比较,hello2.elf中的基本信息未发生改变(如Magic,种别等),而范例发生改变,程序头大小和节头数目增长,并且获得了入口地址。
图 26 ELF头环境
[*]节头
hello2.elf中的节头包罗了文件中出现的各个节的语义,包罗节的范例、位置、偏移量和大小等信息。与hello.elf相比,其在链接之后的内容更加丰富具体(此处仅截取部分展示)。
图 27 节头环境
[*]程序头
程序头部分是一个布局数组,描述了体系准备程序执行所需的段或其他信息。
图 28 程序头部分
[*]Dynamic section
图 29 Dynamic Section
[*]Symbol table
符号表中生存着定位、重定位程序中符号界说和引用的信息,全部重定位必要引用的符号都在此中声明(此处仅截取部分展示)。
图 30 Symbol table
5.4 hello的虚拟地址空间
打开edb,通过 data dump 查看加载到虚拟地址的程序代码。
图 31 加载到虚拟地址的程序代码
根据计算机体系的特性,程序被载入至地址0x400000~0x401000中。在该地址范围内,每个节的地址都与前一节中节对应的 Address 相同。根据edb查看的效果,在地址空间0x400000~0x400fff中存放着与地址空间0x400000~0x401000相同的程序,在0x400fff之后存放的是.dynamic到.shstrtab节的内容。
5.5 链接的重定位过程分析
在Shell中使用命令objdump -d -r hello > hello2.asm天生反汇编文件hello2.asm,与第四章中天生的hello.o.asm文件进行比较,其差别之处如下:
图 32 天生.asm文件
[*]链接后函数数目增长。链接后的反汇编文件hello2.asm中,多出了.plt,puts@plt,printf@plt,getchar@plt,exit@plt,sleep@plt等函数的代码。这是由于动态链接器将共享库中hello.c用到的函数参加可执行文件中。
图 33 链接后的函数
[*]函数调用指令call的参数发生变化。在链接过程中,链接器解析了重定位条目,call之后的字节代码被链接器直接修改为目标地址与下一条指令的地址之差,指向相应的代码段,从而得到完备的反汇编代码。
图 34 call指令的参数
[*]跳转指令参数发生变化。在链接过程中,链接器解析了重定位条目,并计算相对间隔,修改了对应位置的字节代码为PLT 中相应函数与下条指令的相对地址,从而得到完备的反汇编代码。
图 35 跳转指令的参数
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的全部过程。请列出其调用与跳转的各个子程序名或程序地址。
表格 7 程序名称与程序地址
程序名称
程序地址
ld-2.27.so!_dl_start
0x7fce8cc38ea0
ld-2.27.so!_dl_init
0x7fce8cc47630
hello!_start
0x400500
libc-2.27.so!__libc_start_main
0x7fce8c867ab0
-libc-2.27.so!__cxa_atexit
0x7fce8c889430
-libc-2.27.so!__libc_csu_init
0x4005c0
hello!_init
0x400488
libc-2.27.so!_setjmp
0x7fce8c884c10
-libc-2.27.so!_sigsetjmp
0x7fce8c884b70
--libc-2.27.so!__sigjmp_save
0x7fce8c884bd0
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
0x7fce8cc4e680
-ld-2.27.so!_dl_fixup
0x7fce8cc46df0
--ld-2.27.so!_dl_lookup_symbol_x
0x7fce8cc420b0
libc-2.27.so!exit
0x7fce8c889128
5.7 Hello的动态链接分析
编译器没有办法预测函数的运行时地址,以是必要添加重定位记录,等待动态链接器处置惩罚,为制止运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,在GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数,在加载时,动态链接器会重定位GOT中的每个条目,使得它包罗目标的正确的绝对地址。
.got与.plt节生存着全局偏移量表GOT,其内容从地址0x601000开始。通过edb查看,在dl_init调用前,其内容如下:
图 36 调用前的环境
在调用后,其内容变为:
图 37 调用后的环境
比较可以得知,0x601008~0x601017之间的内容,对应着全局偏移量表GOT和GOT的内容发生了变化。GOT生存的是指向已经加载的共享库的链表地址。GOT是动态链接器在ld-linux.so模块中的入口。这样,接下来执行程序的过程中,就可以使用过程链接表PLT和全局偏移量表GOT进行动态链接。
5.8 本章小结
本章具体说明确链接的机制和作用,并展示了可重定位文件hello.o到可执行文件hello的转变过程。使用READELF和GDB工具展示了hello.o和hello的ELF格式和各个节的内容。到此查看了hello的虚拟地址计算、重定位过程、动态链接过程,最终hello.c可以被执行。
第6章 hello进程管理
[*]
[*] 进程的概念与作用
[*]进程的概念
进程是一个正在运行的程序的实例,体系中的每一个程序都运行在某个进程的上下文中。
[*]进程的作用
给应用程序提供两个关键抽象:
[*]一个独立的逻辑控制流,提供一个假象,好像程序独占地使用处置惩罚器
[*]一个私有地址空间,提供一个假象,好像程序独占地使用内存体系
6.2 简述壳Shell-bash的作用与处置惩罚流程
Shell 的作用:
Shell 是一个用C语言编写的交互型应用程序,代表用户运行其他程序。Shell 应用程序提供了一个界面,用户可以通过这个界面进行体系的基本操作,访问操作体系内核的服务。
Shell的处置惩罚流程大抵如下:
[*]从Shell终端读入输入的命令。
[*]切分输入字符串,获得并识别全部的参数
[*]若输入参数为内置命令,则立刻执行
[*]若输入参数并非内置命令,则调用相应的程序为其分配子进程并运行
[*]若输入参数非法,则返回错误信息
[*]处置惩罚完当前参数后继续处置惩罚下一参数,直到处置惩罚完毕
6.3 Hello的fork进程创建过程
打开Shell,输入命令./hello 学号 姓名,带参数执行天生的可执行文件。
fork进程的创建过程如下:首先,带参执行当前目录下的可执行文件hello,父进程会通过fork函数创建一个新的运行的子进程hello。子进程获取了与父进程的上下文,包罗栈、通用寄存器、程序计数器,环境变量和打开的文件相同的一份副本。子进程与父进程的最大区别是有着跟父进程不一样的PID,子进程可以读取父进程打开的任何文件。当子进程运行结束时,父进程假如仍然存在,则执行对子进程的回收,否则就由init进程回收子进程。
图 38 程序执行环境
6.4 Hello的execve过程
调用函数fork创建新的子进程之后,子进程会调用execve函数,在当进步程的上下文中加载并运行一个新程序hello。execve 函数从不返回,它将删除该进程的代码和地址空间内的内容并将其初始化,然后通过跳转到程序的第一条指令或入口点来运行该程序。将私有的区域映射进来,比方打开的文件,代码、数据段,然后将公共的区域映射进来。后面加载器跳转到程序的入口点,即设置PC指向_start 地址。_start函数最终调用hello中的 main 函数,这样,便完成了在子进程中的加载。
6.5 Hello的进程执行
在程序运行时,Shell为hello fork了一个子进程,这个子进程与Shell有独立的逻辑控制流。在hello的运行过程中,若hello进程不被抢占,则正常执行;若被抢占,则进入内核模式,进行上下文切换,转入用户模式,调理其他进程。直到当hello调用sleep函数时,为了最大化使用处置惩罚器资源,sleep函数会向内核发送哀求将hello挂起,并进行上下文切换,进入内核模式切换到其他进程,切换回用户模式运行抢占的进程。与此同时,将 hello 进程从运行队列参加等待队列,由用户模式酿成内核模式,并开始计时。当计时结束时,sleep函数返回,触发一个中断,使得hello进程重新被调理,将其从等待队列中移出,并内核模式转为用户模式。此时 hello 进程就可以继续执行其逻辑控制流。
6.6 hello的非常与信号处置惩罚
[*]在程序正常运行时,打印十次提示信息,以输入回车为标志结束程序,并回收进程。
图 39 程序正常执行
[*]在程序运行时按回车,会多打印几处空行,程序可以正常结束。
图 40 程序运行时按下回车
[*]按下Ctrl + C,Shell进程收到SIGINT信号,Shell结束并回收hello进程。
图 41 程序运行时按Ctrl + C
[*]按下Ctrl + Z,Shell进程收到SIGSTP信号,Shell体现屏幕提示信息并挂起hello进程。
图 42 程序运行时按Ctrl + Z
对hello进程的挂起可由ps和jobs命令查看,可以发现hello进程确实被挂起而非被回收,且其job代号为1。
图 43 用ps命令查看挂起进程
在Shell中输入pstree命令,可以将全部进程以树状图体现(此处仅展示部分):
图 44 用pstree命令查看全部进程
输入kill命令,则可以杀死指定(进程组的)进程:
图 45 kill命令杀死指定进程
输入fg 2则命令将hello进程再次调到前台执行,可以发现Shell首先打印hello的命令行命令,hello再从挂起处继续运行,打印剩下的语句。程序仍然可以正常结束,并完成进程回收。
图 46 用fg命令将进程调回前台
[*]不绝乱按
在程序执行过程中乱按所造成的输入均缓存到stdin,当getchar的时候读出一个’\n’结尾的字串(作为一次输入),hello结束后,stdin中的其他字串会当做Shell的命令行输入。
图 47 不绝乱按的环境
6.7本章小结
在本章中,我们阐释了进程的含义和作用,同时介绍了shell的一样寻常处置惩罚流程和作用。我们还借助hello理解了shell怎样用fork创建子进程,execve执行进程,以及体系的非常范例与信号处置惩罚方法。
第7章 hello的存储管理
7.1 hello的存储器地址空间
[*]逻辑地址
逻辑地址是指由程序产生的与段相干的偏移地址部分,逻辑地址由选择符和偏移量两部分组成。具体而言,其为hello.asm中的相对偏移地址。
[*]线性地址
逻辑地址颠末段机制转化后为线性地址,其为处置惩罚器可寻址空间的地址,用于描述程序分页信息的地址。具体以hello而言,线性地址标志着 hello 应在内存上哪些具体数据块上运行。
[*]虚拟地址
根据CSAPP课本,虚拟地址即为上述线性地址。
[*]物理地址
CPU通过地址总线的寻址,找到真实的物理内存对应地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
Intel处置惩罚器从逻辑地址到线性地址的变换通过段式管理的方式实现。每个程序在体系中都生存着一个段表,段表生存着该程序各段装入主存的状况信息,包罗段号或段名、段起点、装入位、段的长度、主存占用区域表、主存可用区域表等,从而方便进行段式管理。
在段寄存器中,存放着段选择符,可以通过段选择符来得到对应段首地址。段选择符的布局如下:
图 48 段选择符的环境
其包罗三部分:索引,TI,RPL
索引:用来确定当前使用的段描述符在描述符表中的位置;
TI:根据TI的值判定选择全局描述符表(TI=0,GDT)或选择局部描述符表(TI=1,LDT);
RPL:判定紧张品级。RPL=00,为第0级,位于最高级的内核,RPL=11,为第3级,位于最低级的用户状态;
通过一个索引,可以定位到段描述符,进而通过段描述符得到段基址。段基址与偏移量结合就得到了线性地址,虚拟地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址(VA)到物理地址(PA)之间的转换通过对虚拟地址内存空间进行分页的分页机制完成。
通过7.2节中的段式管理过程,可以得到了线性地址/虚拟地址,记为VA。虚拟地址可被分为两个部分:VPN(虚拟页号)和VPO(虚拟页偏移量),根据计算机体系的特性可以确定VPN与VPO的具体位数,由于虚拟内存与物理内存的页大小相同,因此VPO与PPO(物理页偏移量)一致。而PPN(物理页号)则需通过访问页表中的页表条目(PTE)获取,如下图所示。
图 49 Hello的线性地址到物理地址的变换-页式管理
若PTE的有效位为1,则发生页掷中,可以直接获取到物理页号PPN,PPN与PPO共同组成物理地址。
若PTE的有效位为0,说明对应虚拟页没有缓存到物理内存中,产生缺页故障,调用操作体系的内核的缺页处置惩罚程序,确定牺牲页,并调入新的页面。再返回到原来的进程,再次调用导致缺页的指令。此时发生页掷中,获取到PPN,与PPO共同组成物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
针对Intel Core i7 CPU研究VA到PA的变换。
Intel Core i7 CPU的基本参数如下:
[*]虚拟地址空间48位(n=48)
[*]物理地址空间52位(m=52)
[*]TLB四路十六组相连
[*]L1,L2,L3块大小为64字节
[*]L1,L2八路组相连
[*]L3十六路组相连
[*]页表大小4KB(P=4x1024=2^12),四级页表,页表条目(PTE)大小8字节
由上述信息可以得知,VPO与PPO有p=12位,故VPN为36位,PPN为40位。单个页表大小4KB,PTE大小8字节,则单个页表有512个页表条目,必要9位二进制进行索引,而四级页表则必要36位二进制进行索引,对应着36位的VPN。TLB有16组,故TLBI有t=4位,TLBT有36-4=32位。
图 50 TLB与四级页表支持下的VA到PA的变换
如图所示, CPU产生虚拟地址VA,并将其传送至MMU,MMU使用前36位VPN作为TLBT(前32位)+TLBI(后4位)在TLB中进行匹配,若掷中,则得到PPN(40bit)与VPO(12bit)组合成物理地址PA(52bit)。若TLB没有掷中,则MMU向页表中查询,由CR3确定第一级页表的起始地址,VPN1(9bit)确定在第一级页表中的偏移量,查询出PTE,假如在物理内存中且权限符合,则执行下一步确定第二级页表的起始地址,以此类推,最终在第四级页表中查询到PPN,与VPO组合成PA,并向TLB中添加条目。多级页表的工作原理展示如下:
s
若查询PTE的时候发现不在物理内存中,则引发缺页故障。假如发现权限不敷,则引发段错误。
7.5 三级Cache支持下的物理内存访问
由于三级Cache的工作原理基本相同,以是在这里以L1 Cache为例,介绍三级Cache支持下的物理内存访问。
L1 Cache的基本参数如下:
[*]8路64组相连
[*]块大小64字节
由L1 Cache的基本参数,可以分析知:
块大小64字节→必要6位二进制索引→块偏移6位
共64组→必要6位二进制索引→组索引6位
余下标记位→必要PPN+PPO-6-6=40位
故L1 Cache可被分别如下(从左到右):
CT(40bit)CI(6bit)CO(6bit)
在7.4中已经由虚拟地址VA转换得到了物理地址PA,首先使用CI进行组索引,每组8路,对8路的块分别匹配CT(前40位)假如匹配乐成且块的valid标志位为1,则掷中(hit),根据数据偏移量CO取出相应的数据后返回。
若没有匹配乐成大概匹配乐成但是标志位是1,则不掷中(miss),向下一级缓存中哀求数据(哀求顺序为L2 Cache→L3 Cache→主存,若仍不掷中才继续向下一级哀求)。查询到数据之后,必要对数据进行读入,一种简单的放置策略如下:若映射到的组内有空闲块,则直接放置在空闲块中,若当前组内没有空闲块,则产生冲突(evict),采用LFU策略进行更换。
7.6 hello进程fork时的内存映射
当fork函数被当进步程hello调用时,内核为新进程hello创建各种数据布局,并分配给它一个唯一的PID。为了给这个新的hello创建虚拟内存,它创建了当进步程的mm_struct、区域布局和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域布局都标记为私有的写时复制。
当fork在新进程中返回时,新进程如今的虚拟内存刚好和调用fork时存在的虚拟内存相同。当着两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射
execve函数加载并运行hello必要以下几个步调:
[*]删除已存在的用户区域
删除当进步程hello虚拟地址的用户部分中的已存在的区域布局。
[*]映射私有区域
为新程序的代码、数据、bss和栈区域创建新的私有的、写时复制的区域布局。此中,代码和数据区域被映射为hello文件中的.text和.data区。bss区域是哀求二进制零的,映射到匿名文件,其大小包罗在hello中。栈和堆区域也是哀求二进制零的,初始长度为零。
[*]映射共享区域
若hello程序与共享对象或目标(如尺度C库libc.so)链接,则将这些对象动态链接到hello程序,然后再映射到用户虚拟地址空间中的共享区域内。
[*]设置程序计数器
末了,execve设置当进步程上下文中的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处置惩罚
发生一个缺页非常后,控制会转移到内核的缺页处置惩罚程序。判定虚拟地址是否正当,若不正当,则产生一个段错误,然后终止这个进程。
若操作正当,则缺页处置惩罚程序从物理内存中确定一个牺牲页,若该牺牲页被修改过,则将它换出到磁盘,换入新的页面并更新页表。当缺页处置惩罚程序返回时,CPU 再次执行引起缺页的指令,将引起缺页的虚拟地址重新发送给MMU。由于虚拟页面如今缓存在物理内存中,以是就会掷中,主存将所哀求字返回给处置惩罚器。
7.9动态存储分配管理
动态内存管理的基本方法与策略介绍如下:
动态内存分配器维护着一个称为堆的进程的虚拟内存区域。分配器将堆视为一组差别大小的块的集合来维护。每个块就是一个一连的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放可以由应用程序显式执行或内存分配器自身隐式执行。
具体而言,分配器分为两种基本风格:显式分配器、隐式分配器。
显式分配器:要求应用显式地释放任何已分配的块。
隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾网络。
下面介绍动态存储分配管理中较为紧张的概念:
[*]隐式链表
堆中的空闲块通过头部中的大小字段隐含地连接,分配器通过遍历堆中全部的块,从而间接遍历整个空闲块的集合。
对于隐式链表,其布局如下:
图 51 隐式链表的布局
[*]显式链表
在每个空闲块中,都包罗一个前驱(pred)与后继(succ)指针,从而镌汰了搜索与适配的时间。
显式链表的布局如下:
图 52 显式链表的布局
[*]带边界标记的合并
采取使用边界标记的堆块的格式,在堆块的末了为其添加一个脚部,其为头部的副本。添加脚部之后,分配器就可以通过检查前面一个块的脚部,判定前面一个块的起始位置和状态。从而实现快速合并,减小性能消耗。
[*]分离存储
维护多个空闲链表,此中,每个链表的块具有相同的大小。将全部可能的块大小分成一些等价类,从而进行分离存储。
7.10本章小结
本章主要介绍了hello 的存储器地址空间、intel 的段式管理、hello 的页式管理, VA 到PA 的变换、物理内存访问,hello进程fork、execve 时的内存映射、缺页故障与缺页中断处置惩罚、动态存储分配管理。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模子化:文件
设备管理:unix io接口
全部的IO设备都被模子化为文件,而全部的输入和输出都被当做对相应文件的读和写来执行。这种将设备映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。这使得全部的输入和输出都能以一种同一且一致的方式来执行:打开文件、改变当前的文件位置、读写文件、关闭文件。
8.2 简述Unix IO接口及其函数
[*]Unix I/O接口:
[*]打开文件
一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的全部操作中标识这个文件,内核记录有关这个打开文件的全部信息。对于Shell创建的每个进程,其都有三个打开的文件:尺度输入,尺度输出,尺度错误。
[*]改变当前的文件位置
对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序可以或许通过执行seek,显式地将改变当前文件位置k。
[*]读写文件
一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增长到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
[*]关闭文件
内核释放文件打开时创建的数据布局,并将这个描述符恢复到可用的描述符池中去。
[*]Unix I/O函数:
[*]int open(char* filename,int flags,mode_t mode)
进程通过调用open函数来打开一个存在的文件或是创建一个新文件的。open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明确进程计划怎样访问这个文件,mode参数指定了新文件的访问权限位。
[*]int close(fd)
fd是必要关闭的文件的描述符,close返回操作效果。
[*]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的实现分析
查看windows体系下的printf函数体:
图 53 printf的函数体
形参列表中的…是可变形参的一种写法,当传递参数的个数不确定时,用这种方式来表示。
va_list的界说:typedef char *va_list,说明它是一个字符指针,此中 (char*)(&fmt) + 4) 即arg表示的是...中的第一个参数。
再进一步查看windows体系下的vsprintf函数体:
图 54 vsprintf的函数体
则知道vsprintf程序按照格式fmt结合参数args天生格式化之后的字符串,并返回字串的长度。
在printf中调用体系函数write(buf,i)将长度为i的buf输出。write函数如下:
printf函数的功能为担当一个格式化命令,并按指定的匹配的参数格式化输出,故i = vsprintf(buf, fmt, arg)是得到打印出来的字符串长度,其后的write(buf, i)是将buf中的i个元素写到终端。
因此,vsprintf的作用为担当确定输特殊式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,进而产生格式化输出。
再进一步对write进行追踪:
图 55 write的环境
这里给几个寄存器传递了参数,然后以一个int INT_VECTOR_SYS_CALL结束。INT_VECTOR_SYS_CALL代表通过体系调用syscall,查看syscall的实现:
图 56 syscall的环境
syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码,符体现驱动子程序:从ASCII到字模库到体现vram(存储每一个点的RGB颜色信息)。体现芯片按照刷新频率逐行读取vram,并通过信号线向液晶体现器传输每一个点(RGB分量)。
8.4 getchar的实现分析
异步非常-键盘中断的处置惩罚:键盘中断处置惩罚子程序。担当按键扫描码转成ascii码,生存到体系的键盘缓冲区。
getchar等调用read体系函数,通过体系调用读取按键ascii码,直到担当到回车键才返回。
getchar调用体系函数read,发送一个中断信号,内核抢占这个进程,用户输入字符串,键入回车后(字符串和回车都生存在缓冲区内),再次发送信号,内核重新调理这个进程,getchar从缓冲区读入字符。
8.5本章小结
本章主要介绍了Linux的IO设备管理方法、Unix IO接口及函数,分析了printf和getchar函数。这些可以或许帮助我们更好地理崩溃系级I/O的工作模式。
结论
hello程序的一生履历了如下过程:
[*]预处置惩罚
将hello.c中include的全部外部的头文件头文件内容直接插入程序文本中,完成字符串的更换,方便后续处置惩罚;
[*]编译
通过词法分析和语法分析,将正当指令翻译成等价汇编代码。通过编译过程,编译器将hello.i 翻译成汇编语言文件 hello.s;
[*]汇编
将hello.s汇编程序翻译成机器语言指令,并把这些指令打包成可重定位目标程序格式,最闭幕果生存在hello.o 目标文件中;
[*]链接
通过链接器,将hello的程序编码与动态链接库等网络整理成为一个单一文件,天生完全链接的可执行的目标文件hello;
[*]加载运行
打开Shell,在此中键入 ./hello 1190200208 李旻翀,终端为其fork新建进程,并通过execve把代码和数据加载入虚拟内存空间,程序开始执行;
[*]执行指令
在该进程被调理时,CPU为hello其分配时间片,在一个时间片中,hello享有CPU全部资源,PC寄存器一步一步地更新,CPU不断地取指,顺序执行自己的控制逻辑流;
[*]访存
内存管理单元MMU将逻辑地址,一步步映射成物理地址,进而通过三级高速缓存体系访问物理内存/磁盘中的数据;
[*]动态申请内存
printf 会调用malloc 向动态内存分配器申请堆中的内存;
[*]信号处置惩罚
进程时刻等待着信号,假如运行途中键入ctr-c ctr-z 则调用shell 的信号处置惩罚函数分别进行制止、挂起等操作,对于其他信号也有相应的操作;
[*]终止并被回收
Shell父进程等待并回收hello子进程,内核删除为hello进程创建的全部数据布局。
附件
文件名
功能
hello.i
预处置惩罚后得到的文本文件
hello.s
编译后得到的汇编语言文件
hello.o
汇编后得到的可重定位目标文件
hello.elf
用readelf读取hello.o得到的ELF格式信息
hello.asm
反汇编hello.o得到的反汇编文件
hello2.elf
由hello可执行文件天生的.elf文件
hello2.asm
反汇编hello可执行文件得到的反汇编文件
参考文献
Randal E.Bryant, David O'Hallaron. 深入理解计算机体系. 机器工业出版社.2018.4
Pianistx.printf 函数实现的深入分析.2013.
https://www.cnblogs.com/pianist/p/3315801.html.
空想之家xiao_chen.ELF文件头更具体布局.2017.
https://blog.csdn.net/qq_32014215/article/details/76618649.
Florian.printf背后的故事.2014.
https://www.cnblogs.com/fanzhidongyzby/p/3519838.html.
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页:
[1]