摘 要
为了深入理解一个步伐从无到有,从运行到终止的过程,本文论述了hello.c在编写完成后运行在Linux系统中的生命历程,运用相关工具展示了hello.c文件预处理、编译、汇编、链接、运行、回收等阶段并进行分析。同时介绍了shell的内存管理,IO管理,历程管理等相关知识,了解假造内存、非常信号等内容,通过本次大作业实现对书本中知识的更深入理解。
关键词:计算机系统;编译;历程;假造内存
目 录
第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 本章小结............................................................................. - 10 -
第3章 编译.................................................................................. - 11 -
3.1 编译的概念与作用............................................................. - 11 -
3.2 在Ubuntu下编译的下令.................................................. - 11 -
3.3 Hello的编译结果解析...................................................... - 12 -
3.4 本章小结............................................................................. - 19 -
第4章 汇编................................................................................. - 20 -
4.1 汇编的概念与作用............................................................. - 20 -
4.2 在Ubuntu下汇编的下令.................................................. - 20 -
4.3 可重定位目标elf格式...................................................... - 20 -
4.4 Hello.o的结果解析........................................................... - 24 -
4.5 本章小结............................................................................. - 27 -
第5章 链接................................................................................. - 28 -
5.1 链接的概念与作用............................................................. - 28 -
5.2 在Ubuntu下链接的下令.................................................. - 28 -
5.3 可实行目标文件hello的格式......................................... - 29 -
5.4 hello的假造地址空间....................................................... - 33 -
5.5 链接的重定位过程分析..................................................... - 34 -
5.6 hello的实行流程............................................................... - 37 -
5.7 Hello的动态链接分析...................................................... - 38 -
5.8 本章小结............................................................................. - 39 -
第6章 hello历程管理.......................................................... - 40 -
6.1 历程的概念与作用............................................................. - 40 -
6.2 简述壳Shell-bash的作用与处理流程........................... - 40 -
6.3 Hello的fork历程创建过程............................................ - 40 -
6.4 Hello的execve过程........................................................ - 41 -
6.5 Hello的历程实行.............................................................. - 41 -
6.6 hello的非常与信号处理................................................... - 42 -
6.7本章小结.............................................................................. - 47 -
第7章 hello的存储管理...................................................... - 48 -
7.1 hello的存储器地址空间................................................... - 48 -
7.2 Intel逻辑地址到线性地址的变动-段式管理.................. - 48 -
7.3 Hello的线性地址到物理地址的变动-页式管理............. - 49 -
7.4 TLB与四级页表支持下的VA到PA的变动................... - 50 -
7.5 三级Cache支持下的物理内存访问................................ - 52 -
7.6 hello历程fork时的内存映射......................................... - 52 -
7.7 hello历程execve时的内存映射..................................... - 53 -
7.8 缺页故障与缺页中断处理................................................. - 54 -
7.9动态存储分配管理.............................................................. - 55 -
7.10本章小结............................................................................ - 56 -
第8章 hello的IO管理....................................................... - 57 -
8.1 Linux的IO装备管理方法................................................. - 57 -
8.2 简述Unix IO接口及其函数.............................................. - 57 -
8.3 printf的实现分析.............................................................. - 59 -
8.4 getchar的实现分析.......................................................... - 61 -
8.5本章小结.............................................................................. - 62 -
结论............................................................................................... - 62 -
附件............................................................................................... - 64 -
参考文献....................................................................................... - 65 -
第1章 概述
1.1 Hello简介
Hello的P2P是指,hello程颠末预处理,编译,汇编,链接得到可实行目标文件。
- 步伐员在文本编辑器中编写步伐的源代码,保存为hello.c文件。
- 预处理阶段:预处理器根据以#开头的下令来修改原始步伐生成hello.i文本文件。
- 编译阶段:编译器将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言步伐,该步伐包含函数main的定义。
- 汇编阶段:汇编器将hello.s翻译成机器语言指令,打包成可重定位目标步伐的格式,结果保存在hello.o。
- 链接阶段:链接器将目标文件与库文件链接在一起,生成可实行文件。
图 1 编译系统
Hello的020是指hello怎样在历程中实行并被回收的过程。
通过shell输入./hello下令开始实行步伐,shell通过fork函数创建它的子历程,再由子历程实行execve函数加载hello,在execve函数实行hello步伐后,内核为其映射假造内存、分配物理内存,步伐开始实行,内核为步伐分配时间片实行逻辑控制流,当hello运行结束,shell接受到相应的信号,启动信号处理机制,对该历程进行回收处理,释放其所占的内存并删除有关历程上下文。
1.2 环境与工具
硬件环境:12th Gen Intel(R) Core(TM) i9-12900H 2.50 GHz x64
软件环境:windows 11 64位;Ubuntu 22.04.03;VMware 16.2.2 build-19200509
使用工具:codeblocks;gcc;gdb;edb;objdump
1.3 中心结果
(1)hello.c:源代码
(2)hello.i:预处理后的文本文件
(3)hello.s:编译后的汇编文件
(4)hello.o:汇编后的可重定位目标实行文件
(5)hello:链接之后的可实行文件
(6)hello_elf.txt:用readelf读取hello.o得到的ELF格式信息
(7)hello1_elf.txt:用readelf读取hello得到的ELF格式信息
(8)hello.txt:hello反汇编的结果
(9)hello_o.txt:hello.o反汇编的结果
1.4 本章小结
本章重要简朴介绍了hello的P2P,020过程,而且扼要介绍了P2P和020是什么,展示了一个源步伐颠末预处理、编译、汇编、链接等阶段,最终成为一个可实行目标文件的过程,最后列出了本次实行的实行环境和使用工具以及实行过程产生的中心文件。
第2章 预处理
2.1 预处理的概念与作用
预处理是编程中编译源代码的第一步,它在编译器开始工作之前实行。在C、C++等语言中,预处理器不是一个编译器的组成部分,而是一个独立的步伐,它读取源代码文件,根据预处理指令(常见的有#include、#define、#ifndef、#endif等等)来处理源步伐的文本,然后生成一个修改后的源文件,这个文件随后将被编译器处理。
预处理的作用:
- 宏更换:预处理器会处理源代码中的宏定义(通常用#define指令定义),它会将全部宏调用更换为它们的值或定义的代码块,通过使用宏名来代替一段字符串,方便步伐员编写步伐,有利于代码的简洁性和可读性。
- 文件包含:#include指令告诉预处理器将另一个文件的内容包含进来,可以用来共享代码,比如头文件,保证了之后的编译过程可以或许精确进行。
- 条件编译:预处理器支持条件编译,允许根据差别的条件包含或排除代码块,通常通过#ifdef、#ifndef、#endif等指令实现,可以有效删去一些无用代码,从而减少编译过程的工作量。
2.2在Ubuntu下预处理的下令
使用gcc -E hello.c -o hello.i指令进行预处理
图 2 gcc预处理指令
2.3 Hello的预处理结果解析
源步伐hello.c
图 3 源步伐
预处理后hello.i文本文件(部分截图)
图 4 预处理后文件
图 5 预处理后文件
预处理后,文件的格式仍为文本文件,文件的行数增加到了3092行,预处理后的文件中没有了#include等代码,阐明预处理过程中将#include包含的文件加入到了源步伐中。比方stdio.h是尺度库文件,预处理器会到Linux系统的环境变量下探求stdio.h,打开/usr/include/stdio.h。若stdio.h使用了“#define”“#include” 等,对它们进行递归展开更换,对于此中使用的“#ifdef”、“#ifndef”等条件编译语句,预处理器会对条件值进行判定来决定是否对此部分进行包含。
原来的hello.c文件中的表明都被删除,余下的原步伐部分没有发生任何厘革,在hello.i文件的结尾。
hello.i文件开头为源步伐的相关信息。
图 6 hello.i文件开头信息
随后依次进行头文件stdio.h unistd.h stdlib.h的展开。
图 7 hello.i中头文件展开
范例定义信息
图 8 hello.i中范例定义信息
函数声明信息
图 9 hello.i中函数声明信息
hello.c源代码在hello.i文件结尾,除表明和以“#”开头的语句被删除外,其他内容保持稳固。
图 10 hello.i中结尾信息
2.4 本章小结
本章介绍了预处理的相关概念、作用和linux系统中的预处理指令,并通过检察hello.i文件和hello.c文件对比他们的差别,分析了预处理的过程与结果。预处理过程实质上来说是文本增加、删除和更换的过程,是一个通过宏展开、宏更换、插入头文件等操纵,使得步伐中的宏引用被递归地更换掉的过程,生成.i文件后交给编译器进行处理。
第3章 编译
3.1 编译的概念与作用
C编译器在进行具体的步伐翻译之前,会先对源步伐进行词法分析和语法分析,然后根据分析的结果进行代码优化和存储分配,最终把C语言源步伐翻译成汇编语言步伐。总的说来,编译是指将预处理后的.i文件翻译成汇编语言步伐.s文件的过程,这一过程由C编译器(ccl)完成。
编译的作用:
- 词法分析:编译器起首将源代码分解成一个个的词素,词素是编程语言中的基本元素,如关键字、标识符、操纵符等。
- 语法分析:编译器根据编程语言的语法规则,将词素组织成语法树,确保源代码的结构符合语言规范。
- 语义分析:编译器查抄语法树中的每个节点,确保它们在语义上是精确的。比方,它会查抄变量是否已被声明、范例是否匹配等。
- 中心代码:源步伐的一种内部表示,或称中心语言。中心代码的作用是可使编译步伐的结构在逻辑上更为简朴明白,特别是可使目标代码的优化比较轻易实现中心代码。
- 优化:编译器对中心代码进行优化,以提高步伐的实行服从。优化可以是局部的,也可以是全局的,目的是减少资源斲丧和提高性能。
- 目标代码:目标代码生成器把语法分析后或优化后的中心代码变动成目标代码,此处指目标代码为汇编代码。可以说,编译的作用是通过一系列步骤让源代码更接近机器语言,编译是汇编阶段翻译成机器语言的条件。
3.2 在Ubuntu下编译的下令
使用gcc -m64 -no-pie -fno-PIC -S hello.i -o hello.s指令进行编译
图 11 gcc编译指令
图 12 编译后文件
3.3 Hello的编译结果解析
3.3.1数据
数据包罗常量、变量(全局/局部/静态)、表达式、范例、宏,大作业步伐中包含常量和局部变量,接下来对这两部分进行分析。
在hello.c中printf函数括号内的字符串"用法: Hello 学号 姓名 秒数!\n"以及"Hello %s %s\n"。
图 13 printf字符串
LC1里存放了"Hello %s %s\n",LC0存放了字符串"用法: Hello 学号 姓名 秒数!\n",汉字全部被更换成了“\+三个数字”的形式,这是因为在hello.s文件中汉字的表示是接纳UTF-8编码,并用8进制表示,出来每个“\+三个数字”的结构表示一个字节。比方,“用”的UTF-8编码是E7 94 A8(16进制),将其16进制表示成8进制就成为了\347 \224 \250,英语字符的表示在hello.s中是正常表示的。两个字符串的信息都被放在了.rodata节中
图 14 hello.s中的字符串
在汇编语言中,$后加数字表示立刻数。如c步伐中argc!=5中的5就表示为$5。
图 15 hello.s中的整型数
局部变量在汇编语句中被放在寄存器里或栈里,本步伐中有局部变量int i,i被存储在-4(%rbp)中,初始化为0,i占据了4字节的地址空间,关于局部变量i的进一步使用将在背面解析for循环时展示。
图 16 hello.s中的局部变量
argc是用户通报给main函数的参数,被放在了堆栈中。
图 17 hello.s中的参数argc
在hello.s中,其首地址保存在栈中,访问时通过寄存器寻址的方式访问。起始地址为%rbp-32,通过addq $8,%rax addq $16,%rax addq $24,%rax分别得到argv[1]和argv[2]和argv[3]。
图 18 hello.s中的数组argv
3.3.2赋值
赋值重要由mov指令实现,mov指令根据操纵数的字节大小可以分为:movb:一个字节,movw:两个字节,movl:四个字节,movq:八个字节。
图 19 hello.s中的赋值
3.3.3算数运算
++操纵,如将i++操纵表示为
图 20 hello.s中的算数操纵
3.3.4控制转移
(1)if语句
步伐里开头使用了一个条件判定if(argc!=5),它被编译器翻译为cmp加上条件转移指令的形式。
图 21 源步伐中的if语句
图 22 if语句的汇编实现
cmpl语句将edi寄存器的值和立刻数5比较(实际上就是计算%edi-4),如果相等(计算结果为0),就将ZF标志设为1;如果不相等(计算结果不为0),就将ZF标志设为0。根据ZF的值进行条件跳转,从而实现了if语句的条件分支。根据je .L2语句,当ZF=1时,即argc等于5的时候,就会实行.L2标签内的代码;当ZF=0时,即argc不等于5的时候,步伐就往下次序实行。
(2)for循环语句
图 23 源步伐中的for循环语句
图 24 for循环语句的汇编实现
起首为i赋初值为0。for循环的判定条件(i<10),当i<10的时候步伐会实行for循环的循环体,通过cmpl语句比较寄存器ebp的值和常量9,此时cmpl会根据%ebp-7的结果修改多个标志的值,如果ebp的值小于9时,cmpl会将OF标志的值设为1,将SF标志的值设为1;如果ebp的值等于9,cmpl会将ZF标志的值设为1,jle条件跳转的跳转条件是(SF^OF|ZF),因此当ebp的值小于等于7的时候,jle的跳转条件成立,从而会实行for循环的循环体。
每次for循环后实行i++操纵(上面介绍算数运算时提到),继续实行.L3标签内的代码,进行比较,若i小于等于9操纵同上,若大于9则跳出循环实行后续操纵。
3.3.5main函数
main函数被系统函数调用实行。
参数通报:main函数有两个参数int argc和char *argv[],两个参数分别通过寄存器edi和rsi通报表示。
图 25 main函数参数通报
参数char*argv[]表示的是一个字符指针数组,在参数-m64下进行编译,因此每一个指针的大小应该为8个字节,数组的访问在数据部分已经介绍。
返回值: main函数的返回值通过代码return 0实现,在hello.s中是通过设置eax寄存器实现。通过movl语句,将eax寄存器的值设置为0,从而设置了main函数的返回值。
图 26 hello.s中main函数返回值实现
3.3.6普通函数
参数通报:64位栈结构中是通过寄存器和内存共同实现的,第1~6个参数存放在寄存器%rdi,%rsi,%rdx,%rcx,%r8,%r9中,寄存器不够用时把多的参数存放在栈中。
函数调用:call指令会将返回地址压入栈中,而且将%rip的值设置为指向所调用函数的地址(等函数实行完之后调用ret弹出原来的%rip而且将栈帧结构恢复)。
函数的返回值通过寄存器eax保存。
图 27 调用printf函数
4个参数,把第一个参数字符串地址放在rdi中,第二个参数argv[1]放在rsi中,第三个参数argv[2]放在rdx中,第四个参数argv[4]放在rcx中,然后call printf,从printf返回。
图 28 调用exit函数
将1传给%edi,完成参数通报,call exit进行函数调用,从exit返回。
图 29 调用sleep函数
编译器关于函数嵌套的处理与一般的处理是类似的,先处理内层函数,再将内层函数的返回值作为外层函数的参数,最后再处理外层函数。
将atoi的返回值%eax通过%rdi通报给sleep函数,call sleep调用sleep函数,从sleep中返回。
将argv[4]通过%rdi通报给atoi函数,call atoi进行函数调用,从atoi中返回。
图 30 调用getchar函数
无参数,调用后从getchar返回。
3.4 本章小结
本章介绍了编译的概念与作用,编译是将文本文件翻译成汇编语言步伐,为后续将其转化为二进制机器码做准备的过程,由之前的C语言步伐,变化成了汇编语言步伐。以hello.c源步伐为例进行编译,对结果进行解析,解析了汇编代码怎样实现数据、赋值、算术操纵、控制转移、函数调用等,加深对编译以及汇编语言的理解。
第4章 汇编
4.1 汇编的概念与作用
汇编的概念:汇编是将编译后产生的.s汇编文件翻译成机器语言指令的过程。这一过程由汇编器(as)完成。
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制步伐的过程。
汇编的作用:汇编的作用是将汇编语言代码转换成机器语言代码,并将结果保存在可重定位目标文件(.o二进制文件)中。汇编过程从汇编步伐得到一个可重定位目标文件,以便后续进行链接。
4.2 在Ubuntu下汇编的下令
使用gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o指令进行汇编
图 31 gcc汇编指令
4.3 可重定位目标elf格式
ELF可重定位目标文件的典型格式
图 32 ELF可重定位文件典型格式
使用readelf -a hello.o > hello_elf.txt指令检察hello.o的ELF格式并保存到hello_elf.txt文件中。
图 33 检察hello.o的ELF格式文件
图 34 hello.o的ELF格式文件
4.3.1ELF头
ELF头以一个16字节的序列(Magic)开始,这个序列形貌了生成该文件的系统的字的大小和字节次序。ELF头剩下的部分包含帮助链接器语法分析息争释目标文件的信息,此中包罗ELF头的大小,目标文件的范例(可重定位、可实行大概是共享的)、机器范例(x86-64)、节头部表的文件偏移、以及节头部表中条目的大小和数目等。
图 35 ELF头信息
4.3.2节头部表
在节头部表里形貌了差别节的名称、范例、地址、偏移量和大小等信息。
图 36 节头部表
4.3.3重定位节
当汇编器生成一个目标模块时,它并不知道数据和代码最终将放在内存中的什么位置,它也不知道这个模块引用的任何外部定义的函数大概全局变量的位置。以是,无论何时汇编器遇到对最终位置未知的目标引用,它就会生成一个重定位条目,告诉链接器在将目标文件归并成可实行文件时怎样修改这个引用。
重定位节记录了各段引用的符号相关信息,在链接时,需要通过重定位节对这些位置的地址进行重定位。链接器会通过重定位条目的范例判定怎样计算地址值并使用偏移量等信息计算出精确的地址。
图 37 重定位节
4.3.4symtab节
符号表(.symtab)存放着步伐中定义和引用的函数和全局变量的信息。
图 38 symtab节
4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,并与第3章的 hello.s进行对照分析。
图 39 hello.o反汇编
(1)数的表示:hello.s中的操纵数是十进制,hello.o反汇编代码中的操纵数是十六进制。
图 40 hello.s中操纵数
图 41 hello.o反汇编中的操纵数
(2)函数调用:在hello.s中调用函数,是call +函数名,而在hello.o反汇编中调用函数是通过PC相对寻址的方式,定位函数的地址。因为函数只有在链接之后才能确定运行实行的地址,因此在.rela.text节中为其添加了重定位条目。
图 42 hello.s中的函数调用
图 43 hello.o反汇编中的函数调用
(3)汇编中mov、push、sub等指令都有表示操纵数大小的后缀(b : 1字节、w :2 字节、l :4 字节、q :8字节),反汇编得到的代码中则没有。hello.s中提供给汇编器的辅助信息在反汇编代码中不再出现,如“.cfi_def_cfa_offset 16”等。
图 44 hello.s中的指令
图 45 hello.o反汇编中的指令
(4)在hello.s中进行跳转,是跳转到某一个标签所关联的代码,使用格式串的时候,也是使用标签。而在hello.o的反汇编中,不再使用标签,而是用地址来代替(只不外由于需要重定位,因此hello.o的反汇编中跳转指令之后是相对偏移的地址,即间接地址)。
图 46 hello.s中的跳转
图 47 hello.o反汇编中的跳转
(5)反汇编代码除了汇编代码之外,还表现了机器代码,在左侧用16进制表示。
机器语言是用二进制代码表示的计算性能直接识别和实行的一种机器指令的集合。一条机器语言指令由操纵码+操纵数构成。操纵码规定了指令的操纵,是指令中的关键字,不能缺省。操纵数表示该指令的操纵对象。
机器语言与汇编语言的映射关系:汇编语言通过助记符来表示机器指令,并通过汇编器转换成机器语言。对于汇编语言的每一个指令如movq、leaq、popq等在机器语言中都有操纵码与之对应,而且对于操纵的寄存器差别,操纵码也会有差别。
在汇编语言中,操纵数可以是立刻数、寄存器、内存地址或常量。然而,在机器语言中,全部的操纵数都必须通过地址来引用。
在汇编语言中,可以使用标签或条件语句(如JMP, JE, JNE)来实现分支转移,汇编器将这些转换为包含跳转地址的机器指令。
在汇编语言中,函数调用通常通过调用指令(如CALL)和返回地址来实现。汇编器需要处理这些指令,生成包含目标函数地址的机器指令,并在栈上保存返回地址。
4.5 本章小结
本章介绍了汇编的概念与作用, hello.s文件汇编为hello.o文件,并生成hello.o的ELF格式文件写入hello_elf.txt,检察并总结了hello.o的elf文件格式的信息。同时使用反汇编检察hello.o颠末反汇编过程生成的代码并与hello.s相比较,发现机器语言与汇编语言之间的映射关系和差别。
第5章 链接
5.1 链接的概念与作用
链接的概念:链接是将各种代码和数据片段收集并合成为一个单一文件的过程,这个文件可被加载(复制)到内存并实行,这一过程由链接器完成。链接可以实行于编译时,也就是在源代码被翻译成机器代码时;也可以实行于加载时,也就是在步伐被加载器加载到内存并实行时;甚至实行于运行时,也就是由应用步伐来实行。
注意:这儿的链接是指从 hello.o 到hello生成过程。
链接的作用:链接使我们的步伐可以或许乐成访问到它引用的全部目标模块,从而保证了我们的可实行步伐可以在机器上顺利实行。链接可以实现分离编译,可以借助链接的优势将大型的应用步伐分解成更小、更加易于管理的模块,使得各模块之间的修改都和编译相互独立。
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指令进行链接
图 48 ld链接指令
5.3 可实行目标文件hello的格式
使用readelf -a hello > hello1_elf.txt指令检察hello的ELF格式并保存到hello1_elf.txt文件中。
图 49 检察hello的ELF格式
5.3.1ELF头
以形貌了生成该文件的系统的字的大小和字节次序的16字节序列Magic开始,剩下的部分包含帮助链接器语法分析息争释目标文件的信息,如步伐入口点的地址,步伐头起点等。在可重定位目标文件的elf中,入口点地址和步伐头起点是0,当步伐被链接后生成的可实行文件中的elf中都被填入了精确的地址。
图 50 ELF头
5.3.2节头
在节头里形貌了节的名称、范例、地址、偏移量、大小等信息。
图 51 节头部表
5.3.3步伐头
步伐头里形貌了差别段的范例,段在文件中的偏移,段在假造内存中的地址,段在物理内存里的地址,段在文件中的大小,段在内存中的大小,段的属性(可读、可写、可实行)以及对齐等信息,形貌了可实行文件中的节与假造空间中的存储段之间的映射关系
图 52 步伐头
5.3.4重定位节
图 53 重定位节
5.3.5符号表
符号表中增加了很多内容。
图 54 symtab表
5.3.6Dynamic section
与可重定位目标文件相比,可实行文件增加了一个Dynamic section。
图 55 Dynamic section
5.4 hello的假造地址空间
使用edb加载hello,检察本历程的假造地址空间各段信息,并与5.3对照分析阐明。
假造地址空间的起始地址为0x400000。
图 56 假造地址空间起始地址
使用edb检察Loaded Symbols
图 57 Loaded Symbols
据节头部表,我们可以知道.rodata节开始于假造地址0x402000处,因此在edb中通过检察地址0x402000的内容可以找到位于.rodata节的格式串。
图 58 假造内存中的.rodata节
.inerp段的起始地址为04002e0。
图 59 假造内存中的.inerp段
.text段的起始地址为0x4010f0,0x4010f0处是步伐的起始位置。
图 60 假造内存中的.text段
5.5 链接的重定位过程分析
通过objdump -d -r hello 对hello文件进行反汇编,结果如下:
图 61 hello的反汇编
在hello.o中跳转指令和call指令后为相对地址,而在hello中已经是重定位之后的假造地址。
图 62 hello.o中的相对地址
图 63 hello反汇编中的假造地址
hello反汇编中还多了一些节如.plt节等、多了外部函数的PLT信息、以及_start入口的代码。
图 64 .plt节内容
图 65 外部函数的PLT信息
图 66 _start入口
链接的过程:链接的过程重要包罗符号解析和重定位两个步骤。在符号解析的过程中,链接器将每一个符号引用与它输入的可重定位目标文件中的符号表中的一个确定的符号定义关联起来。一旦链接器完成了符号解析这一步,就会开始重定位步骤。在重定位步骤里,链接器将全部雷同范例的节归并为同一范例的新的聚合节,然后链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每一个节,以及赋给输入模块定义的每个符号。之后,链接器依靠重定位条目,针对重定位条目中的每个符号,修改代码节和数据节对这些符号的引用,使它们指向精确的运行时的地址。
重定位:一开始,链接器会进行节的归并以及将运行时内存地址赋给聚合节,赋给输入模块定义的每一个节,以及赋给输入模块定义的每个符号。完成了这一步的时候,代码中引用的每个符号都有了唯一的运行时的内存地址。根据重定位条目中的内容,修改代码节和数据节中对每个符号的引用。由于在重定位条目中有形貌符号的范例(是PC相对引用或是绝对引用),也形貌了重定位符号的偏移量(引用重定位符号的相对于.text节大概.data节的位置)以及一个特别的加数。用offset表示重定位符号的偏移量,用addend表示这个特别的加数。
如果符号是PC相对引用的,由于已经知道每一个节在运行时的内存地址(ADDRs)。那么引用这个重定位符号的位置refaddr,可以通过refaddr=offset+ADDRs计算得到,同时由于该重定位符号运行时内存地址已知(设为ADDRb),那么修改后的地址应该为ADDRb-refaddr+addend。如果符号是绝对引用,那么修改后的地址应该就是这个重定位符号运行时的内存地址。
由下图中可以知道exit函数运行时在内存中的地址为0x4010d0。在第四章的重定位条目中知道exit的偏移量为0x29,加数为-4(因为32位地址,占了4个字节)。由第五章之前的节头部表知.text节运行时在内存的地址为0x4010f0,从而可以计算得需要修改对exit的引用的位置为0x4010f0+0x29=0x401119,从而可以计算得修改后的结果为0x4010d0-0x401119+(-0x4)=0xffffff7e,从而重定位后修改的结果为0xffffff7e,在hello的反汇编中是小端表示,因此操纵码背面应该是7e ff ff ff。
图 67 exit运行时的内存地址
图 68 .text节运行时的内存地址
图 69 hello中调用exit
5.6 hello的实行流程
- 调用了动态链接库linux-x86-64.so.2、libc.so中的几个函数
- _start
- __libc_start_main
- __cxa_atexit
- libc.so中几个函数
- 动态链接库libc.so.6里的函数
- hello!main
- hello!puts@plt
- hello!exit@plt
- hello! printf@plt
- hello!sleep@plt
- hello!getchar@plt
5.7 Hello的动态链接分析
节头部表中有如下信息:
在dl_init前,.got节.got.plt节的内容如下:
在dl_init后,.got节.got.plt节的内容如下:
.got节、.got.plt节在内存里的内容都发生了改变。
在dl_init调用之前,对于每一条PIC函数调用,调用的目标地址都实际指向PLT中的代码,GOT存放的是PLT中函数调用指令的下一条指令地址。在函数调用时,起首跳转到PLT实行.plt中操纵,第一次访问跳转时GOT地址为下一条指令,将函数序号入栈,然后跳转到PLT[0],之后将重定位表地址入栈,访问动态链接器,在动态链接器中使用在栈里保存的函数序号和重定位表计算函数运行时的地址,重写GOT,返回调用函数。之后如果另有对该函数的访问,就不用实行第二次跳转,直接参看GOT信息。
5.8 本章小结
本章对hello.o进行链接,得到可实行文件hello,检察了hello的elf格式并对其进行了分析,检察了hello的假造空间地址的各段信息,利用objdump检察hello.o与hello的差别,分析阐明了在链接过程中的重定位过程,检察了hello的实行流程,经历的函数,最后对hello的动态链接进行了扼要的分析。
第6章 hello历程管理
6.1 历程的概念与作用
历程的定义是一个可实行中步伐的实例。系统中的每个步伐都运行在某个历程上下文中。上下文是由步伐精确运行所需的状态组成的,包罗存放在内存中的步伐的代码和数据、它的栈、通用目标寄存器的内容、步伐计数器、环境变量以及打开文件形貌符的集合。
在现代系统上运行一个步伐时,我们会得到一个假象,就好像我们的步伐是系统中当前运行的唯一的步伐一样,我们的步伐好像是独占地使用处理器和内存,处理器就好像是无中断地一条接一条地实行我们步伐中的擅令,我们步伐中的代码和数据好像是系统内存中唯一的对象,这些假象都是通过历程的概念提供给我们的。
6.2 简述壳Shell-bash的作用与处理流程
shell是一个交互型应用级步伐,代表用户运行其他步伐。它通过实行一系列的读/求值步骤,读取用户的下令行,解析下令,然后代表用户运行步伐。Shell的功能重要有负责各历程创建与步伐加载运行及前后台控制,作业调用,信号发送与管理等。
处理流程:
(1)终端历程读取用户由键盘输入的下令行。
(2)分析下令行字符串,获取下令行参数,并构造通报给execve的argv向量。
(3)查抄第一个下令行参数是否是一个内置的shell下令。
(3)如果不是内部下令,调用fork( )创建新历程/子历程。
(4)在子历程中,用步骤2获取的参数,调用execve( )实行指定步伐。
(5)如果用户没要求后台运行(下令末端没有&号)否则shell使用waitpid(或wait...)等候作业终止后返回。
(6)如果用户要求后台运行(如果下令末端有&号),则shell返回。
6.3 Hello的fork历程创建过程
输入下令实行 hello 后,父历程如果判定不是内部指令,即会通过 fork 函数创建子历程。子历程与父历程相似,并得到一份与父历程用户级假造空间雷同且独立的副本(包罗数据段、代码、共享库、堆和用户栈)。父历程打开的文件子历程也可读写,二者之间的PID的差别。fork函数只会被调用一次,但会返回两次,在父历程中,fork返回子历程的PID,在子历程中,fork返回0。
父历程与子历程是并发运行的独立历程,内核可以或许以任意方式瓜代实行它们的逻辑控制流的指令。在子历程实行期间,父历程默认选项是表现等候子历程的完成。
6.4 Hello的execve过程
当fork之后,子历程调用execve函数(传入下令行参数)在当前历程的上下文中加载并运行一个新步伐即hello步伐,execve调用驻留在内存中的被称为启动加载器的操纵系统代码来实行hello步伐,加载器删除子历程现有的假造内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零,通过将假造地址空间中的页映射到可实行文件的页大小的片,新的代码和数据段被初始化为可实行文件中的内容。最后加载器设置PC指向_start地址,_start最终调用hello中的main函数。除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制。直到CPU引用一个被映射的假造页时才会进行复制,这时,操纵系统利用它的页面调度机制自动将页面从磁盘传送到内存。需要注意的是,execve乐成调用的时候不会返回到调用步伐,只有出现错误了,才会返回到调用步伐。
6.5 Hello的历程实行
上下文信息:上下文就是内核重新启动一个被抢占的历程所需要的状态,它由通用寄存器、浮点寄存器、步伐计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。
逻辑控制流:一系列步伐计数器PC的值的序列叫做逻辑控制流,历程是轮流使用处理器的,在同一个处理器核心中,每个历程实行它的流的一部分后被抢占(临时挂起),然后轮到其他历程。
时间片:一个历程实行它的控制流的一部分的每一时间段叫做时间片。为了实现多个历程轮流运行,操纵系统会给历程分配时间片。当操纵系统调度hello历程的时候,系统会给hello历程分配时间片,在这一时间片里,hello历程可以独占地使用处理器。
用户模式和内核模式:处理器通常使用一个寄存器提供两种模式的区分,该寄存器形貌了历程当前享有的特权,当没有设置模式位时,历程就处于用户模式中,用户模式的历程不允许实行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,历程处于内核模式,该历程可以实行指令集中的任何下令,而且可以访问系统中的任何内存位置。而一个历程从用户模式变为内核模式的唯一方法是通过诸如中断、故障大概陷阱如许的非常。上下文切换一定发生在内核模式下。
hello sleep历程调度的过程:调用sleep之前,若hello步伐不被抢占则次序实行,若发生被抢占的情况,则进行上下文切换,并进行如下操纵:
(1)保存以前历程的上下文
(2)恢复新恢复历程被保存的上下文
(3)将控制通报给这个新恢复的历程 ,来完成上下文切换。
hello初始运行在用户模式,在hello历程调用sleep之后陷入内核模式,内核处理休眠哀求主动释放当前历程,并将hello历程从运行队列中移出加入等候队列,定时器开始计时,内核进行上下文切换将当前历程的控制权交给其他历程,当定时器到时时发送一个中断信号,此时进入内核状态实行中断处理,将hello历程从等候队列中移出重新加入到运行队列,成为就绪状态,hello历程就可以继续进行自己的控制逻辑流了。
图 70 历程上下文切换的剖析
6.6 hello的非常与信号处理
6.6.1正常实行
图 71 正常实行状态
6.6.2乱按键盘
在按键盘的时候会发生中断非常,hello历程会进入内核模式,将控制转移给中断非常处理步伐。键盘的中断处理步伐,会从键盘控制器的寄存器读取扫描码并翻译成ASCII码,并存入键盘缓冲区。在按了回车键的时候,输入的字符串会被shell识别为下令。
图 72 乱按键盘后的实行结果
6.6.3Ctrl-Z
历程收到SIGTSTP信号,信号的动作是将hello挂起,用ps下令可以检察当前的全部历程的历程号,用jobs下令看到job ID是1,状态是“已制止”。
图 73 ctrl+z
输入pstree下令:以树状图表现历程间的关系。
图 74 pstree
用fg下令可以将指定的作业放在前台运行,此时会给指定的历程组发送SIGCONT信号,让挂起的历程重新运行。用kill下令可以向指定的历程组发送信号,kill -9表示发送SIGINT信号,会让历程组内每一个历程终止。
图 75 fg kill
6.6.4Ctrl-C
在hello步伐运行时输入CTRL+C会导致内核发送一个SIGINT信号到前台历程组的每个历程,默认情况下,结果是终止前台作业。
图 76 ctrl+c
6.6.5总结
1. 中断:异步发生,是来自处理器外部的I/O装备的信号的结果。
处理:在当前指令的实行过程中,中断引脚电压变高,在当前指令完成后,控制通报给处理步伐,中断处理步伐运行,处理步伐返回下一条指令。
2. 陷阱:同步发生,有意的非常,是实行一条指令的结果。
处理:应用步伐实行一次系统调用,控制通报给处理步伐,陷阱处理步伐运行,处理步伐返回到syscall之后的指令。
3. 故障:同步发生,由错误情况引起的,大概可以或许被故障处理步伐修正。
处理:当前指令导致一个故障,控制通报给处理步伐,故障处理步伐运行
处理步伐要么重新实行当前指令,要么终止。
4. 终止:同步发生,不可恢复的致命错误造成的结果,不将控制返回给应用步伐。
处理:发生致命的硬件错误,控制通报给处理步伐,终止处理步伐运行,处理步伐返回到abort例程。
6.7本章小结
本章重要介绍了hello在shell中是怎样运行的,分析了hello实行过程中的历程管理。通过历程的概念和shell的工作流程,分析shell是怎样通过调用fork函数为hello创建子历程,execve函数加载hello函数的,利用时间片的概念,分析的内核的历程调度过程,用户态和内核态的转换,最后分析了hello实行过程中遇到的非常和信号。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:是指由步伐产生的与段相关的偏移地址部分,是步伐代码颠末编译后出现在汇编步伐中地址。一个逻辑地址由一个段和偏移量组成。
线性地址:逻辑地址颠末段机制后转化为线性地址(假造地址),是逻辑地址到物理地址变动之间的中心层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。如果启用了分页机制,那么线性地址可以再颠末变动以产生一个物理地址;如果没有启用分页机制,那么线性地址直接就是物理地址。
假造地址:假造地址空间和线性地址空间是雷同的。
物理地址:是指出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变动的最闭幕果地址。对于系统中物理内存的M个字节,都有{0,1,2,...,M-1}中的一个数与之逐一对应,这个数就是该字节的物理地址。
7.2 Intel逻辑地址到线性地址的变动-段式管理
(以下格式自行编排,编辑时删除)
一个逻辑地址由两部分组成:段标识符:段内偏移量。段标识符是一个16位长的字段(段选择符),可以通过段标识符的前13位,直接在段形貌符表中找到一个具体的段形貌符,这个形貌符就形貌了一个段。
索引号就是“段形貌符”的索引,段形貌符具体地址形貌了一个段。很多个段形貌符,就组成了一个数组,叫“段形貌符表”,可以通过段标识符的前13位,直接在段形貌符表中找到一个具体的段形貌符,每一个段形貌符由8个字节组成。全局的段形貌符,放在“全局段形貌符表(GDT)”中,一些局部的段形貌符,放在“局部段形貌符表(LDT)”中。
在保护模式下,段寄存器中存放着段选择符。16位段选择符(由13位索引,1位TI,2位RPL组成)。TI位表示索引的形貌符表种别(TI=0,选择全局形貌符表(GDT),TI=1,选择局部形貌符表(LDT)),高13位索引可以用来确定当前使用的段形貌符在形貌符表中的位置,RPL表示特权级别。
在实际转换的时候,通过TI位决定去访问GDT照旧LDT,然后根据13位索引去查找选定段的段形貌符,通过段形貌符中的内容可以得到段基址,将段基址与EA(偏移量)相加就得到了线性地址。
图 77 逻辑地址向线性地址转换
7.3 Hello的线性地址到物理地址的变动-页式管理
将步伐的逻辑地址空间划分为固定大小的页,而物理内存划分为同样大小的页框。步伐加载时可将任意一页放入内存中任意一个页框,这些页框不必一连,从而实现了离散分配。在页式存储管理方式中地址结构由两部分构成,前一部分是假造页号(VPN),后一部分是假造页偏移量(VPO)。
页表:页表将假造内存映射到物理地址空间,每次地址翻译硬件将一个假造地址转换为一个物理地址时,都会读取页表。页表是一个页表条目(PTE)的数组。假造地址空间中每个页在页表中一个固定偏移量处都有一个PTE,假设每个PTE是由是由一个有效位和n位地址字段组成的。有效位表明了该假造页当前是否被缓存在DRAM中,如果设置了有效位,那么地址字段就表示DRAM中相应的物理页的起始位置,这个物理页缓存了这个假造页;如果没有设置有效位,那么一个空地址表示这个页还没有被分配。
CPU里面有一个控制寄存器PTBR(页表基址寄存器),指向当前页表。通过PTBR找到页表的首地址,再根据VPN的值可以得到对应的页表条目的地址(PTEA)。PTEA=%PTBR+VPN*页表条目大小。
找到了页表条目后,如果有效位=1,阐明该假造页缓存进了内存,从而根据PTE可以找到该假造页对应的物理页号。由于假造页和物理页大小相等,物理页中的页内偏移PPO=VPO。从而物理地址由PPN与VPO组合而成。
图 78 使用页表的地址翻译
7.4 TLB与四级页表支持下的VA到PA的变动
CPU产生一个假造地址(VPN和VPO的组合)
图 79 假造地址中用以访问TLB的组成部分
根据TLB索引(TLBI)去选择快表(TLB)中对应的组号,再根据TLB标志去匹配相应的路,如果匹配乐成了,那么可以得到页表条目PTE,之后将PTE中的PPN与VPO组合,从而可以得到物理地址(PA)。如果匹配失败,快表中没有缓存这一页表条目,那么需要按照7.3中的步骤,从页表基址寄存器中得到页表的首地址,然后根据VPN的值,可以得到页表条目的地址PTEA=%PTBR+VPN*页表条目大小。根据PTEA的值,在高速缓存大概内存中取出PTE的值,并将新取出的PTE存放在快表中,得到了PTE后,就可以按照同样的方法得到PA。
图 80 TLB命中和不命中的操纵图
CPU产生一个假造地址,假造地址由VPN1,VPN2,VPN3,VPN4和VPO组合而成。
由于PTBR页表基址寄存器存放了一级页表的首地址,因此通过VPN1可以检察一级页表中,存放的相应的二级页表的地址,再通过VPN2检察二级页表中,存放的相应的三级页表的地址,再通过VPN3检察三级页表中,存放的相应的四级页表的地址,最后通过VPN4得到四级页表中相应的页表条目(PTE)。根据PTE可以得到物理页号PPN,通过将PPN与VPO组合从而得到了物理地址PA。
图 81 使用k级页表的地址翻译
7.5 三级Cache支持下的物理内存访问
高速缓存存储器(Cache)组织结构:
根据PA、L1高速缓存的组数和块大小确定高速缓存块偏移(CO)、组索引(CI)和高速缓存标志(CT),使用CI进行组索引,对组中每行的标志与CT进行匹配。如果匹配乐成且块的valid标志位为1,则命中,然后根据CO取出数据并返回数据给CPU。
若未找到相匹配的行或有效位为0,则L1未命中,继续在下一级高速缓存(L2)中进行类似过程的查找。若仍未命中,还要在L3高速缓存中进行查找。三级Cache均未命中则需访问主存获取数据。若进行了上述操纵,阐明至少有一级高速缓存未命中,则需在得到数据后更新未命中的Cache。起首判定此中是否有空闲块,若有空闲块(有效位为0),则直接将数据写入;若不存在,则需根据更换策略(如LRU、LFU策略等)驱逐一个块再写入。
7.6 hello历程fork时的内存映射
当fork函数被当前历程调用时,内核为新历程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新历程创建假造内存,它创建了当前历程的mm_struct、地区结构和页表的原样副本。它将两个历程中的每个页面都标志为只读,并将两个历程中的每个地区结构都标志为私有的写时复制。
当fork在新历程中返回时,新历程现在的假造内存刚好和调用fork时存在的假造内存雷同。当这两个历程中的任何一个厥后进行写操纵时,写时复制机制就会创建新页面,因此,也就为每个历程保持了私有地址空间的抽象概念。
7.7 hello历程execve时的内存映射
execve函数调用驻留在内核地区的启动加载器代码,在当前历程中加载并运行包含在可实行目标文件hello中的步伐,用hello步伐有效地替代了当前步伐。
加载并运行hello需要以下几个步骤:
- 删除已存在的用户地区,删除当前历程假造地址的用户部分中的已存在的地区结构。
- 映射私有地区,为新步伐的代码、数据、bss和栈地区创建新的地区结构,全部这些新的地区都是私有的、写时复制的。代码和数据地区被映射为hello文件中的.text和.data区,bss地区是哀求二进制零的,映射到匿名文件,其大小包含在hello中,栈和堆地址也是哀求二进制零的,初始长度为零。
- 映射共享地区, hello步伐与共享对象libc.so链接,libc.so是动态链接到这个步伐中的,然后再映射到用户假造地址空间中的共享地区内。
- 设置步伐计数器(PC),execve做的最后一件变乱就是设置当前历程上下文的步伐计数器,使之指向代码地区的入口点。
图 82 加载器是怎样映射用户地址空间的地区的
7.8 缺页故障与缺页中断处理
若步伐想要访问某个假造页中的数据的时候,会产生一个假造地址。当MMU(内存管理单元)在试图翻译这个假造地址的时候,会发现该地址地点的假造页没有缓存进内存(即PTE中有效位为0),必须从磁盘中取出,这时候就会发生缺页故障。
缺页中断处理:
- 判定假造地址是否正当。缺页处理步伐搜刮地区结构的链表,把假造地址和每个地区结构中的vm_start和vm_end做比较。如果指令不正当,缺页处理步伐会触发一个段错误,从而终止这个历程。
- 判定内存访问是否正当。比如缺页是否由一条试图对只读页面进行写操纵的指令造成的。如果访问不正当,缺页处理步伐会触发一个保护非常,从而终止这个历程。
- 内核知道缺页是由正当的操纵造成的。内核会选择一个捐躯页面,如果这个捐躯页面被修改过,那么就将它互换出去,换入新的页面并更新页表。处理步伐返回时,CPU重新实行引起缺页的指令,这条指令将再次发送给MMU。这次,MMU能正常地进行地址翻译,不会再产生缺页中断。
图 83 Linux缺页处理
图 84 缺页操纵图
7.9动态存储分配管理
动态内存分配器维护着一个称为堆的历程的假造内存地区。分配器将堆视为一组差别大小的块的集合来维护。每个块就是一个一连的假造内存片,要么是已分配的,要么是空闲的。已分配的块显式地保存为供应用步伐使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放可以由应用步伐显式实行或内存分配器自身隐式实行。
分配器分为两种基本风格:显式分配器、隐式分配器。
1.显式分配器:要求应用显式地释放任何已分配的块。
2. 隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。
分配器有以下几种实现方式:
- 用隐式空闲链表的方式来组织空闲块。堆中的空闲块通过头部中的大小字段隐含地连接,分配器通过遍历堆中全部的块,从而间接遍历整个空闲块的集合。
分配器的放置策略有:首次适配(从头搜刮,遇到第一个符合的块就制止)、下一次适配(从链表中上一次查询结束的地方开始,遇到下一个符合的块制止)和最佳适配(全部搜刮,选择符合的块制止)等。
分割策略:不分割,使用整个空闲块;大概将空闲块分成两部分,第一部分变成分配块,剩下部分变成空闲块,获取额外的堆内存得到符合的空闲块。
归并空闲块的策略:立刻归并(每次释放一个块的时候就归并)、推迟归并、带边界标志的归并。
- 用显式空闲链表的方式来组织空闲块。显式空闲链表结构将堆组织成一个双向空闲链表,在每个空闲块的主体中,都包含一个pred(前驱)和succ(后继)指针。使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数目的线性时间。
维护链表的次序:
- 后进先出的次序,将新释放的块放置在链表的开始处。
- 按照地址次序来维护链表,此中链表中每个块的地址都小于它后继的地址。
- 分离的空闲链表:维护多个空闲链表,此中,每个链表的块具有雷同的大小。将全部大概的块大小分成一些等价类,从而进行分离存储。有两种基本方法:简朴分离存储,分离适配。
7.10本章小结
本章重要介绍了hello的存储器地址空间、intel的段式管理、页式管理,TLB与四级页表支持下的VA到PA的变动、三级cache支持下物理内存访问, hello历程fork时的内存映射、execve时的内存映射、缺页故障与缺页中断处理、动态存储分配管理等内容。
第8章 hello的IO管理
8.1 Linux的IO装备管理方法
装备的模型化:文件
全部IO装备都被模型化为文件,全部的输入和输出都能被当做相应文件的读和写来实行。
装备管理:unix io接口
Linux内核有一个简朴、低级的接口,称为Unix I/O,全部的输入和输出都能以一种同一且一致的方式来实行。
8.2 简述Unix IO接口及其函数
将装备优雅地映射成文件的方式,允许Linux内核引出一个简朴、低级的应用接口,称为Unix I/O,这使得全部的输入和输出都能以一种同一且一致的方式来实行。
- 打开文件:一个应用步伐通过要求内核打开相应的文件,来宣告它想要访问一个I/O装备,内核返回一个小的非负整数,叫做形貌符,它在后续对此文件的全部操纵中标识这个文件,内核记录有关这个打开文件的全部信息。应用步伐只需记着这个形貌符。
- Linux shell创建的每个历程开始时都有三个打开的文件:尺度输入,尺度输出和尺度错误。
- 改变当前的文件位置。
- 读写文件:从文件复制n个字节到内存,从当前文件位置k开始,然后将k增加到k+n;写操纵:从内存复制n个字节到文件,当前文件位置为k,然后更新k。
- 关闭文件:当应用完成了对文件的访问之后,它就关照内核关闭这个文件,作为相应,内核释放文件打开时创建的数据结构,并将这个形貌符恢复到可用的形貌符池中。
8.2.1打开文件
历程通过调用open函数来打开一个已存在的文件大概创建一个新文件。
图 85 open函数
open函数将filename转换为一个文件形貌符,而且返回形貌符数字,返回的形貌符总是在历程中当前没有打开的最小形貌符,flags参数指明了历程打算怎样访问这个文件,mode参数指定了新文件的访问权限位。
8.2.2关闭文件
历程通过调用close函数关闭一个打开的文件。
图 86 close函数
fd是需要关闭的文件的形貌符,关闭一个已关闭的形貌符会出错。
8.2.3读写文件
应用步伐是通过分别调用read和write函数来实行输入和输出的。
图 87 read和write函数
read函数从形貌符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,而返回值0表示EOF。否则,返回值表示的是实际传送的字节数目。
write函数从内存buf复制至多n个字节到形貌符fd的当前文件位置。若乐成则返回值为写的字节数,若出错则为-1。
8.3 printf的实现分析
Printf函数如下
图 88 print函数
参数接纳了可变参数的定义, *fmt是一个char 范例的指针,指向字符串的起始位置。这个指针指向第一个const参数(const char *fmt)中的第一个元素。fmt也是个变量,它的位置,是在栈上分配的,它也有地址。
printf调用的外部函数vsprintf如下:
图 89 vsprintf函数
vsprintf的作用就是格式化,它接受确定输特别式的格式字符串fmt,用格式字符串对个数厘革的参数进行格式化,产生格式化输出,写入buf供系统调用write输出时使用。
write系统函数如下:
图 90 write系统函数
在write函数中,将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址,int INT_VECTOR_SYS_CALLA代表通过系统调用syscall。
sys_call实现如下:
图 91 sys_call实现
ecx中是要打印出的元素个数 ,ebx中的是要打印的buf字符数组中的第一个元素,这个函数的功能就是不断的打印出字符,直到遇到:’\0’ 制止。
总结:函数printf的实现过程调用了vsprintf和write函数,接受一个格式串之后将匹配到的参数按照格式串的形式输出。vsprintf的作用就是格式化。它接受确定输特别式的格式字符串fmt,用格式字符串对个数厘革的参数进行格式化,产生格式化输出。vsprintf函数生成表现信息到write系统函数,到陷阱-系统调用 int 0x80或syscall。字符表现驱动子步伐:从ASCII到字模库到表现vram(存储每一个点的RGB颜色信息)。表现芯片按照刷新频率逐行读取vram,并通过信号线向液晶表现器传输每一个点。
8.4 getchar的实现分析
getchar代码如下:
图 92 getchar函数
当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断哀求,中断哀求抢占当前历程运行键盘中断子步伐,键盘中断子步伐先从键盘接口取得该按键的扫描码,然后将该按键扫描码转换成ASCII码,保存到系统的键盘缓冲区中。
getchar调用read系统函数,read通过syscall(系统调用)调用内核中的系统函数,读取存储在键盘缓冲区中的ASCII码,直到读入回车符,然后返回整个字符串,getchar函数只从中读取第一个字符,其他字符被存放在输入缓冲区。
异步非常-键盘中断的处理:键盘中断处理子步伐。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章介绍了Linux的IO装备管理方法,以及Unix IO接口和函数,同时分析了printf函数和getchar函数的实现方法,体会到Unix IO接口在Linux系统中的重要作用,同时也了解了作为异步非常之一的键盘中断的处理。
结论
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
- 步伐员通过编辑器等进行编写,将hello的源代码写入,存为.c文件,hello.c源步伐诞生。
- 预处理。hello.c文件通过预处理器cpp将调用的库,全部的宏更换掉,归并到hello.i的文本文件中。
- 编译。在编译器中颠末编译,产生了hello.s。
- 汇编。颠末汇编器as将汇编文件转换为机器语言的可重定位二进制目标文件hello.o,此时的hello.o可以或许被机器识别但是由于不完整无法被实行。
- 链接。通过符号解析与重定位,将可重定位目标文件与所依靠的模块链接起来,产生了可以被机器实行的可实行文件hello。
- 运行。在shell中输入下令运行。
- shell调用fork函数为hello创建子历程,在子历程中调用execve函数, 启动加载器,映射假造内存,进入步伐入口之后开始载入物理内存,然后进入main函数。
- CPU为其分配时间片,在每一个时间片中hello享有私有的地址空间,独立实行自己的逻辑控制流。
- CPU访问hello的时候,哀求一个假造地址,MMU把假造地址转换成物理地址,malloc动态申请内存。
- hello在运行中可以收到来自键盘输入的信号,比如CTRL-C,CTRL-Z等。
- hello子历程收到终止信号后就死亡了,在父历程没有调用waitpid函数将其回收前,他一直是僵死历程。
- shell父历程调用waitpid函数将其回收,内核删除了为hello历程创建的全部数据结构。
计算机系统这门课程很复杂,从一个很简朴的步伐hello.c产生到消失经历了复杂的过程,从中我也学习到了很多知识,通过实行以及大作业使我更深入的理解了理论知识,使我收获了很多。通过对这门课程的学习,我已经理解了一些计算机系统的知识,但在后续的学习中仍需不断深入理解,才能不断提拔自己对计算机系统的理解,提拔自己的能力。
附件
列出全部的中心产物的文件名,并予以阐明起作用。
(1)hello.c:源代码
(2)hello.i:预处理后的文本文件
(3)hello.s:编译后的汇编文件
(4)hello.o:汇编后的可重定位目标实行文件
(5)hello:链接之后的可实行文件
(6)hello_elf.txt:用readelf读取hello.o得到的ELF格式信息
(7)hello1_elf.txt:用readelf读取hello得到的ELF格式信息
(8)hello.txt:hello反汇编的结果
(9)hello_o.txt:hello.o反汇编的结果
参考文献
[1] Randal E.Bryant, David R.O'Hallaron.深入理解计算机系统[M]. 北京:机械工业出书社,2016.
[2] http://t.csdnimg.cn/jJrbB
[3] http://t.csdnimg.cn/jozdZ
[4] http://t.csdnimg.cn/DfSvX
[5] 袁东风. 计算机系统根本. 北京:机械工业出书社,2014
[6] https://www.cnblogs.com/pianist/p/3315801.html
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |