摘 要html
摘要是论文内容的高度归纳,应具备独立性和自含性,即不阅读论文的全文,就能得到必要的信息。摘要应包括本论文的目的、主要内容、方法、成果及其理论与实际意义。摘要中不宜使用公式、结构式、图表和非公知公用的符号与术语,不标注引用文献编号,同时避免将摘要写成目录式的内容介绍。linux
计算机系统是高度集成的一个至关复杂的系统,这个系统的实现有多重机制。git
本文经过结束计算机中一个简单的hello程序从预处理一直到IO管理的整个过程当中的实现细节,粗略介绍了计算机系统的机制,对其中一些关键的实现细节进行了相对详细的探究。基于hello的实现过程,本文梳理了一个计算机系统的总体运行流程,可供参考。程序员
关键词:CSAPP P2P 编译 汇编 连接 进程 IO 代码github
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)shell
目 录数据库
第1章 概述 - 4 -express
3.3.1 数据的处理(常量,变量,表达式等) - 10 -
6.2 简述壳Shell-bash的做用与处理流程 - 44 -
7.2 Intel逻辑地址到线性地址的变换-段式管理 - 52 -
7.3 Hello的线性地址到物理地址的变换-页式管理 - 53 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 53 -
7.7 hello进程execve时的内存映射 - 56 -
首先经过键盘向计算机输入一串代码,这串代码组合成了一个hello.c源文件。
接下来将源文件经过gcc编译器预处理,编译,汇编,连接,最终完成一个能够加载到内存执行的可执行目标文件。
接下来经过shell输入文件名,shell经过fork建立一个新的进程,而后在子进程里经过execve函数将hello程序加载到内存。虚拟内存机制经过mmap为hello规划了一片空间,调度器为hello规划进程执行的时间片,使其可以与其余进程合理利用cpu与内存的资源。
而后,cpu一条一条的从hello的.text取指令执行,不断从.data段去除数据。异常处理程序监视着键盘的输入。hello里面的一条syscall系统调用语句使进程触发陷阱,内核接手了进程,而后执行write函数,将一串字符传递给屏幕io的映射文件。
文件对传入数据进行分析,读取vram,而后在屏幕上将字符显示出来。
最后程序运行结束,shell将进程回收,完成了hello程序执行的全过程/
列出你为编写本论文,折腾Hello的整个过程当中,使用的软硬件环境,以及开发与调试工具。
软件:
Visio Studio 2017
VMware
Ubuntu
edb
gdb
gcc
列出你为编写本论文,生成的中间结果文件的名字,文件的做用等。
Hello.c |
Hello的c源代码 |
Hello.i |
源代码预编译产生的ascii文件 |
Hello.s |
Ascii文件编译后产生的汇编代码文件 |
Hello.o |
汇编后产生的可重定位目标文件 |
Hello_o-objdump-d.txt |
可重定位目标文件的对应汇编代码 |
Hello_o-objdump-d-r.txt |
可重定位目标文件的代码及重定位条目 |
Hello_o-readelf-a.txt |
可重定位目标文件的elf条目 |
Hello-ld |
连接生成的可执行目标文件 |
Hello-ld-readelf-a.txt |
可执行目标文件对应的elf条目 |
Hello-objdump-d-r.txt |
可执行目标文件对应的汇编代码 |
Hello-linkinfo.txt |
可执行目标文件的连接信息 |
在本章节中,大体描述了一个hello程序从出生到去世的完整过程,以及我在描述整个过程时所使用的软件环境,和产生的中间文件。
(第1章0.5分)
预处理又称预编译,是指在对C源代码文件进行词法扫描和语法分析以前所作的工做。
预处理所作的主要工做,就是在对一个C源文件进行编译操做以前,对其进行一些预先的处理,包括删除注释,处理宏定义(#define),添加包含的头文件(#include),执行条件编译(#ifdef),一方面可以是生成的与处理文件可以便于编译器的直接处理,也使得编写的程序可以便于阅读,修改,移植和调试,有利于模块化程序设计。
预处理命令:gcc -E hello.c -o hello.i
2-1预处理的效果
右边为预处理前的hello.c源代码,左边为预处理后的hello.i文件,能够看到,源代码中的注释都被删除,而#include命令所包含的头文件都被替代为了相应的代码,这样产生的hello.i文件具备可以独立运行的一套源代码,而不是实现功能的代码片断了。
2-2预处理先后文件的比较
若是说生成一个完整的可执行文件就像是造一辆跑车,那么c源代码就是跑车的图纸,而c文件的预处理就是按照图纸粗制一批合适的钢材。
经过对C文件的预处理,咱们将c文件改编成了统一的格式,经过宏的处理对c文件进行了适当的修正,立刻进入下一步的处理。
(第2章0.5分)
限于C语言,编译就是将用C的语言写成的源代码文件,等价的翻译成汇编语言文件的过程。
编译可以将.i文件中的c代码,不改变其所实现的功能的过程和结果,同时又有必定的优化和修改,翻译成可以同等的完成其任务的一段汇编语言代码,
编译以前,C语言编译器会进行词法分析、语法分析(-fsyntax-only),接着会把源代码翻译成中间语言,即汇编语言。若是想看到这个中间结果,能够用-S选项。
编译程序工做时,先分析,后综合,从而获得目标程序。所谓分析,是指词法分析和语法分析;所谓综合是指代码优化,存储分配和代码生成。为了完成这些分析综合任务,编译程序采用对源程序进行屡次扫描的办法,每次扫描集中完成一项或几项任务,也有一项任务分散到几回扫描去完成的。下面举一个四遍扫描的例子:第一遍扫描作词法分析;第二遍扫描作语法分析;第三遍扫描作代码优化和存储分配;第四遍扫描作代码生成。
值得一提的是,大多数的编译程序直接产生机器语言的目标代码,造成可执行的目标文件,但也有的编译程序则先产生汇编语言一级的符号代码文件,而后再调用汇编程序进行翻译加工处理,最后产生可执行的机器语言目标文件。
gcc -S hello.c -o hello.s
3-1 编译的效果
3-2 编译先后文件的比较
c源代码中出现的数据以下:
3-1 源代码数据类型解析
当前代码中的sleepsecs整型变量就是一个全局变量。全局变量的特色是在C程序的任意函数中都可以直接读写,所以全局变量采用独立于函数以外的存储位置,汇编代码中,全局变量会被存放在函数体外的data段,在运行中经过GOT表进行引用。
3-2汇编代码中的全局变量
汇编代码中同时也对sleepsecs的数据类型进行描述,由于在汇编中是没有整型,浮点型这些概念的,有的只是一串连续的数据。
在汇编程序中,常量通常存放在专门的区域,须要的时候直接调用。为了连接的方便,通常会采起全局偏移量表(GOT)的形式来调用全局变量。
当前的C源代码中的常量主要是两个用于在printf中输出的字符串,这两个字符串直接存放在汇编程序中的只读数据域
3-3 汇编代码中的只读数据
当前c代码中的用于计数的整型变量i就是局部变量,与main函数的参数argc和argv同样,这些数据都是只会在当前的局部函数中进行读写的,外部函数没有可以正常访问到这些数据的方法。所以不须要像全局变量那样在代码段外独立的为这些变量分配空间。
这类数据通常是在程序运行的栈中保存,寄存器中进行传递。同时在栈于寄存器中均可以对其进行修改。
所以,汇编代码为这些数据专门开辟了存储的栈空间。
3-4 开辟栈空间的汇编指令
分别将本来存储于寄存器中的argc于argv变量压入栈中进行管理
3-5将变量压入栈中
变量i也在栈中进行读写管理:
初始化i
3-6初始化i
对i进行累加:
3-7对变量累加
程序最后的栈空间的示意图以下:
3-8栈空间示意图
c源代码中的赋值语句仅有一处:
3-9赋值语句
因为此处的局部变量i存放于栈中,汇编语言直接对栈的值进行修改:
3-10汇编代码中的赋值语句
c源代码中的算术操做仅在for循环语句中有一处:
3-11算术操做
对应的翻译到汇编代码中的形式以下:
3-12汇编代码中的算术操做
c源代码中的关系操做共有两处:
3-13关系操做
分别是argc参数与3进行比较,判断两者是否相等,以及局部变量i与10的比较,判断i的值是否小于10。
c语言中的关系操做表达式的值是根据关系的真伪来肯定的,真为1,假为0。
而在c语言中,关系判断的结果一般用于改变控制流,如做为if语句的判断条件,以及for,while等循环语句中的循环条件。
所以在汇编语言当中,可以直接翻译成相应的条件跳转命令,来决定控制流的方向。
argc变量的关系操做所对应的汇编语言以下
3-14汇编代码中的关系操做
在这里,cmpl命令会怕判断当即数3与参数argc的关系,而后根据结果设置条件寄存器,然后面的je指令经过条件寄存器的值的组合来决定是否跳转。
在这里,若是知足条件argc!=3,就会直接执行下面紧跟着的语句,不然就会跳过这一段语句,直接开始执行L2处的语句。
同理,局部变量i的关系操做对应的汇编代码以下:
3-15汇编代码中的关系操做
只要i的值仍然小于10,就会不断地执行下面的跳转指令,从程序员的角度来看,控制流一直在for循环体内部不断地执行。
值得一提的是,c源代码中的语句是i<10 而这里的语句的等效C语句确是i<=9
因为编译器会对C代码进行优化,毫无疑问这里的c代码也是被优化了的状态,大概在编译器的眼中,判断<=的关系要比判断<的关系的效率更高吧。
这也提醒了咱们,编译器产生的汇编代码不必定是c源代码的简单转换,而是会进行不一样程度的优化,只是最后产生的运行结果没有改变罢了。
指针是c语言编译的一个很是复杂而巧妙的部分。
当前程序中设计到数组/指针操做的代码如图所示:
3-16指针操做
经过对传入的字符串数组argv进行寻址来读取参数。
argv是从命令行键入的字符串的地址数组,里面按顺序存放着命令行输入的字符串在内存中的存放地址。
因为数组是在内存中一段连续的内存空间中进行存储的,因此汇编语言经过索引值与数组基址来对数组内容进行寻址。
argv[1]表明的就是数组中第2个参数的地址,程序员数数都是从0开始的,这难道不是常识吗?
对应的汇编代码以下:
3-17汇编代码中的数组索引处理
经过这样的转换来对数组按照索引进行寻址。
在c语言中,产生控制流转移的状况有两种,分别是分支和循环,在当前函数的代码中对应了if分支判断语句和for循环语句。
3-18控制转移语句
在编译器将c语言中的控制转移语句翻译成为汇编语言时,会使用汇编中的条件判断与跳转指令来约束控制流,使控制流按照c语言所描述的行为来流动。
其中if语句的基本结构:
if(expr)
expression;
若是expr分支判断表达式为真,则执行下面的分支体,不然跳过。
翻译成汇编语言以下:
3-19汇编代码中的分支语句
for语句的基本结构:
for(init-expr; test-expr; update-expr)
body-statement;
按照这个结果等效产生的用goto语句描述的c语句以下:
init-expr;
goto test;
loop:
body-statement
update-expr;
test:
t=test-expr;
if(!t)
goto loop;
对应的汇编代码:
3-20汇编代码中的for循环语句
c语言中的函数调用对应了汇编语言中的call指令,汇编语言与操做系统提供了一整套机制来保证函数多级调用的层进与参数的层层传递可以稳定进行。
在程序运行时,系统会为其提供一个上下文,经过进程机制与虚拟内存机制的配合,在程序看来,本身就好像独自占有内存,且独自占有cpu资源,有本身独立的控制流。这个前提保证了咱们能够忽略系统背后复杂的机制来分析程序自己的运行过程。
基于以上的前提,首先看一下一个程序运行过程当中的内存结构:
3-21程序的运行时内存结构
在程序运行的过程当中,随着函数层层调用,栈不断往下生长。每一个函数都会有一个运行时栈,栈中存放着当前函数运行时所须要的信息,包括局部变量,保存的寄存器。
能够这样想:一个程序的栈能够看做这个程序私有的小内存空间。
栈的先进先出的结构特色与函数的多层调用机制完美契合。
在这样的机制之下,一个函数调用另外一个函数,就在调用函数的栈下面新开辟一个栈空间,而当被调用函数运行结束以后,释放栈空间,就又回到了原来的调用函数的栈空间。
当一个函数被调用的时候,须要记录下返回的地址,这样当这个被调用函数运行结束以后,才能跟顺址寻路,顺利的回到原来的地方继续未竟的事业。
所以当一个函数将要调用下一个函数时,就会将下一个函数调用完成后应该回到的地址放在栈顶,也就是下一个函数栈底一墙之隔的位置。
3-22程序的栈空间
这样当下一个函数执行完成以后,只要顺着本身的栈,就可以找到回家的路。
在栈调用机制的支持下,函数之间的传递参数也变得格外方便。
一个函数想在调用函数的时候传递参数,只须要简单的将参数放在本身的栈当中,下一个函数在运行的时候就能够经过先前函数的栈来读取参数。编译器以及保证了每一个程序都可以正确的找到参数在本身父程序栈中的位置。
更进一步,当参数小于6个的时候,甚至能够不须要经过栈来传递参数,函数能够将本身的参数压入寄存器中,而后由下一个函数到寄存器中去取参数便可。
做为限制,编译器默认的设置了6个专门用来传递参数的寄存器,分别是rdi,rdi,rdx,rcx,r8,r9.当完成了参数传递的任务以后,这几个寄存器有能够看成普通的寄存器来使用。
就这样,经过系统,硬件,编译器,编程语言的相互配合,实现了一套方便的函数调用机制。
在当前程序中,执行函数操做的语句有这些:
3-23函数调用
调用了printf函数与exit函数
3-24函数调用
调用了printf函数与sleep函数
3-25函数调用
调用了getchar函数
在汇编语言中,简单的改用call指令就可以执行对函数的调用:
3-26汇编代码中的函数调用
第一条printf函数调用只传递了一个参数,汇编代码将这个参数传入参数寄存器rdi中,下一个执行的程序就可以直接从rdi寄存器中取值
3-27汇编代码中的函数调用
同理,上一条语句先将当即数1传入参数寄存器中,而后使用call指令调用exit函数。当控制流传递到exit当中时,就会从rdi寄存器中取出1这个数,而后看成退出的状态值。
3-28汇编代码中的函数调用
同理,这几条汇编语句总共将3个参数传入了参数寄存器当中,而后调用printf函数。
3-29汇编代码中的函数调用
用call指令调用getchar函数。
值得注意的是,在当前函数的汇编代码中都跟着一个@PLT符号,这个符号的意思是过程连接表,用于动态库的连接。关于这一部分,将会在后面的连接中继续讨论。
前面我提到预处理后的文件至关于打造出的用于制造跑车的钢材,那么编译这一步就是将钢材细细打磨,变成尺寸严丝合缝的零件。
经过编译,函数的c代码变为了等效的汇编代码,编译器分别从c语言的数据,赋值语句,类型转换,算术操做,逻辑/位操做,关系操做,指针操做,控制转移与函数操做这几个关键点布局,从微观细节上剖析,宏观上调配,既符合了c的语义和用意,有很好的契合了计算机的底层机制。编译器简直就是艺术。
(第3章2分)
汇编会将编译产生的ascii码构成的汇编代码翻译成相对应的机器代码,即目标代码,也就是从人可以读懂的字符翻译成为cpu可以读懂的二进制程序码的过程。
当编译器将c源代码一路翻译成汇编代码以后,仍然不是及其能够读懂的格式。cpu在运行程序时经过机器码来判断所要执行的指令,所以还须要将ascii格式的汇编代码转化为机器码。
但须要注意的是,汇编仍然是一个中间过程。咱们所编写的程序包含着在外部的库中定义的函数,同时也缺乏从系统进入程序的中间函数。
更进一步,当代码越写越大以后,可能会出现更多的定义和引用分离的状况,例如一个函数在一个.c源文件中定义,而被另外一个.c文件中的函数引用。在这种状况下,预处理到编译,不过是将单个的.c文件进行了翻译。
要想程序完整可用,还须要一个将多个文件合并成一个完整的可执行文件的过程,这个过程就是连接,而汇编就是在文件中根据汇编代码生成一些可以指引连接过程进行的数据结构。
形象的说,咱们已经造好了一台汽车的全部零件,在将零件组装起来以前,咱们如今要作的就是打造螺丝钉。
gcc -c hello.s -o hello.o
4-1汇编的效果
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
4-2 elf格式
4-3 elf文件内容图示
一个典型的elf文件的格式如上图所示,根据这个模型,能够简单的分析hello.o文件的基本组成。
首先是ELF头,这个节存储了整个.o文件的一些基本定信息,具体以下图。
4-4 elf文件格式解析
接下来看看节头部表,这张表中存储了elf表中每个节的具体信息,包括类型,名称,偏移值等。以此为索引,可以对elf文件中每个具体的节进行访问。
4-5 elf文件的节头部表
.text节包含着已编译程序的机器代码,具体结构以下:
4-6 elf文件.text段
.rodata节含有例如printf语句中字符串这样的只读数据
.data存放已初始化的全局和静态C变量
.bss存放未初始化的全局和静态C变量,以及全部被初始化为0的全局或静态变量。
.symtab节存放着程序中的全部符号,包括被引用的以及被定义的。连接器能够经过这张表来获取当前可重定位目标文件中的符号信息,并以此来对文件进行连接。
其具体结构以下:
4-7 e文件.symtab节
.rel.text是代码段的重定位条目,每当汇编器发现程序中有未定义的引用或者在当前程序中定义而可能被外部程序所引用的符号(非静态的全局符号),就会为其生成一条重定位条目。
4-8 elf文件的重定位条目
在重定位节当中的每个条目的每一条信息都会在连接的过程当中用于重定位符号,修改引用,将多个可重定位目标文件链接成一个完整的可执行文件。经过可重定位条目中信息的指引,可使连接器准确无误的对多个可重定位目标文件进行合并和修改,具体细节将在连接过程当中具体探讨。
经过objdump指令能够看到hello.o文件的.text段的具体状况,此时的.text段只是一串由1和0构成的机器码,将其对应的转化为汇编指令,会发现一些不一样之处
4-9 可重定位目标文件与汇编代码的区别
共有如下几点不一样:
对比两端代码中的相同跳转语句:
4-10 11跳转语句对比
能够看到,在.o文件中,跳转的位置已经由符号指代变成了具体的数值。
因为不一样文件代码连接合并和,一个文件自己的代码的相对地址不会改变,因此不须要与外部重定位,而能够直接计算出具体的数值,所以这里就已经完成了全部的操做,这条语句将以这种形式加载到内存中被cpu读取与执行。
4-11 重定位条目对比
能够看见,汇编代码文件中的call对函数调用的语句都是直接以函数名来指代,而在.o文件中取而代之的是一条重定位条目指引的信息。
因为调用的这些函数都是未在当前文件中定义的,因此必定要与外部连接才可以执行。
在连接时,连接器将依靠这些重定位条目对相应的值进行修改,以保证每一条语句都可以跳转到正确的运行时位置。
4-12 全局变量引用的重定位条目对比
因为全局变量在运行时的内存位置是未知的,因此一样须要生成一条重定位条目,提醒连接器在连接时谨慎的计算运行时的内存地址,而后分配给每一条引用,保证每一条引用最终都可以指向正确的位置
在.o文件当中,当即数都变为16进制。由于计算机是基于二进制运行的,十六进制能够很方便的与二进制相互转化,所以这里更换成了16进制。
另外一方面,咱们利用objdump看到了翻译过来的汇编代码,但真实的.o文件里保存的其实只有机器码。
在现有的系统中,每个汇编指令都与1个字节的十六进制码一一对应。
好比在这条语句中,mov指令对应的机器码是48,%rsp与%rbp寄存器对应的机器码分别是89和e5,当cpu读取到mov指令后,就立刻解析出这是mov指令,并且后面会跟两个寄存器,所以又会继续读取后面的两个字节,并将其翻译成对应的寄存器,并进行操做。
每个汇编指令对应的操做数的个数与种类都是肯定的,所以一段汇编的机器代码只要肯定一个起始位置,最终解析出来的操做序列是没有二义性的。
要想造出一辆跑车,精密耐用的零件只是一个必要的方面。当零件齐全了以后,如何将零件组装起来,使得每一个零件之间稳固,所以在组装以前,还须要将零件打磨一番。
汇编器对编译器生成的汇编代码文件更深一层,翻译成机器代码文件,也就是可重定位目标文件。因为每一个文件中只有一部分的函数,且文件直接互相引用,互相依赖。与此同时,对于连接器来讲,每一个文件不过是一个字节块,要想解决这些字节块内部之间的互联逻辑,就须要汇编器多作一些,再将汇编代码翻译成机器代码时加入一些可以引导连接器进行连接的数据结构。至此,汇编器的工做就结束了,离成功不过寸步之遥。
(第4章1分)
编译器与汇编器将C源文件简单的改写成等效的汇编代码文件,可是这个文件仍然不够完整,不是一个可以加载到内存中直接开始运行的状态。
由于此时的.o文件,便可重定位目标文件只是一个代码片断,包含了不完整的定义,要想将其变为一个彻底可执行的状态,还须要进行连接
连接是将各类代码和数据片断收集并组合成一个单一文件的过程。
ld -o hello-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 hello.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o
使用ld的连接命令,应截图,展现汇编过程! 注意不仅链接hello.o文件
5-1 连接的过程
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
hello可执行文件的基本结构以下:
5-2 典型的elf可执行目标文件
经过readelf指令读出hello可执行目标文件的elf格式:
elf头:
5-3 可执行目标文件的elf头
节头部表:
5-4 节头
重定位节.rela.text:
5-5 重定位节
重定位节.rela.eh_frame:
5-6 符号节
与可重定位目标文件相比,可执行目标文件被设计成很容易加再到内存的格式。
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
根据linux进程运行时加载到内存的格式的示意图:
5-7 linux进程的虚拟内存映射
经过readelf读取可执行目标文件的程序头表
5-8 目标文件的程序头表
LOAD段起始于0x400000, 表示一个从二进制文件映射到虚拟地址空间的段。其中保存了常量数据(如字符串),程序的目标代码等等。
能够看到,内存中从地址0x0400040开始的一段区域是PHDR段,这一段主要用于保存程序头表
INTERP段起始于0x400200,一样也是只读数据,其主要做用是指定在程序已经从可执行映射到内存以后,必须调用解释器。在这里解释器并不意味着二进制文件的内存必须由另外一个程序解释。它指的是这样的一个程序:经过连接其余库,来知足未解决的引用。
DYNAMIC段起始于0x600e50,保存了其余动态连接器(即,INTERP中指定的解释器)使用的信息。
NOTE保存了专有信息。
程序运行时,就会将相应的信息加载到内存中的对应位置
经过edb动态调试工具查看hello运行时的状态。
从data dump窗口能够看到地址0x0400000到地址0x0401000的运行时内容。
5-9 地址0x0400000到地址0x0401000的运行时内容
能够知道,在程序运行时,这一段区域所存储的内容就是只读数据,即.init,.test,.rodata段,包含程序运行的入口,程序开始运行时配置环境所要调用的系统函数,程序的主体代码以及执行程序时所要用到的一些只读数据。
查看0x0600000到0x0601000的内存内容
5-10 0x0600000到0x0601000的内存内容
查看0x0601000到0x0602000的内存内容
5-11 查看0x0601000到0x0602000的内存内容
以前在解析hello.o文件时,提到了在重定位节当中存放的重定位条目。同时前面也提到过,咱们经过预处理,汇编,编译产生的文件仍然只是最终完整的可执行文件的一部分。完成了hello.o文件的生成,至关于造出了跑车的发动机,发动机确实是跑车最重要的部分,可是只有发动机,跑车也没法工做。hello.o文件中的重定位条目就是指导连接器将其组装起来的说明书。
hello.o文件中的重定位条目以下:
5-12 hello.o文件中的重定位条目
经过objdump指令观察hello.o文件的.text段:
5-13 hello.o文件的.text段
为了方便,objdump工具已经自动将重定位条目放在了相应的位置。
hello可执行目标文件中多出了.init段和.plt段,前者用于初始化程序执行环境,后者用于程序执行时的动态连接,这里再也不赘述。
5-14 两个文件的区别
进行连接以后两个文件的区别如上图,全部的重定位条目都被修改成了肯定的运行时内存地址。
在执行这个连接过程以前,连接器已经经过可重定位目标文件中的符号表信息,肯定的将每一个符号引用都与一处符号定义对应了起来。汇编器生成的重定位条目指明了须要被修改的符号引用的位置,以及有关如何计算被引用修改的一些信息。
对于相对地址的引用,即图中的类型为R_X86_64_PC32的引用条目。
对于这类条目,首先肯定其定义所在的节以及其相对于节的偏移量,经过这两个量计算出符号定义的地址,即ADDR(r.symbol)
接下来经过重定位条目指向的内存位置,对引用信息进行修改,使其指向内存运行时的地址,基本计算公式以下:
5-15 基本计算公式
对于R_X86_64_PLT32类型的引用是动态连接的,也就是在静态连接过程当中只是简单的构造过程连接表(PLT)和全局偏移量表(GOT),而后在程序加载到内存里运行的过程当中才会完成最终的重定位工做。
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的全部过程。请列出其调用与跳转的各个子程序名或程序地址。
使用gdb观察Hello的执行流程:
_init |
0x7ffff7a05920 |
_dl_vdso_vsym |
0x7ffff7b4b3b0 |
_dl_lookup_symbol_x |
0x7ffff7de00b0 |
do_lookup_x |
0x7ffff7ddf240 |
strcmp |
0x7ffff7df2360 |
do_lookup_x |
0x7ffff7ddf240 |
_dl_vdso_vsym |
0x7ffff7b4b3b0 |
_dl_lookup_symbol_x |
0x7ffff7de00b0 |
_dl_vdso_vsym |
0x7ffff7b4b3b0 |
__strrchr_avx2 |
0x7ffff7b723c0 |
__init_misc |
0x7ffff7b056f0 |
__GI___ctype_init |
0x7ffff7a148f0 |
_dl_init |
0x7ffff7de5630 |
init_cacheinfo |
0x7ffff7a05470 |
handle_intel |
0x7ffff7a9fe80 |
intel_check_word |
0x7ffff7a9fb80 |
_start |
0x400500 |
__libc_start_main |
0x7ffff7a05ab0 |
__new_exitfn |
0x7ffff7a27220 |
__GI___cxa_atexit |
0x7ffff7a27430 |
__libc_csu_init |
0x4005c0 |
__sigsetjmp |
0x7ffff7a22b70 |
Main |
0x400536 |
printf@plt |
0x4004c0 |
_dl_runtime_resolve_xsavec |
0x7ffff7dec750 |
_dl_fixup |
0x7ffff7de4df0 |
malloc |
0x7ffff7a052c6 |
sleep@plt |
0x4004f0 |
getchar@plt |
0x4004d0 |
动态连接是一项有趣的技术。考虑一个简单的事实,printf,getchar这样的函数实在使用的太过频繁,所以若是每一个程序连接时都要将这些代码连接进去的话,一份可执行目标文件就会有一份printf的代码,这是对内存的极大浪费。为了遏制这种浪费,对于这些使用频繁的代码,系统会在可重定位目标文件连接时仅仅建立两个辅助用的数据结构,而直到程序被加载到内存中执行的时候,才会经过这些辅助的数据结构动态的将printf的代码重定位给程序执行。便是说,直到程序加载到内存中运行时,它才知晓所要执行的代码被放在了内存中的哪一个位置。
这种有趣的技术被称为延迟绑定,将过程地址的绑定推迟到第一次调用该过程时。而那两个辅助的数据结构分别是过程连接表(PLT)和全局偏移量表(GOT),前者存放在代码段,后者存放在数据段。
首先经过readelf分析可执行目标文件,获得文件的GOTPLT的运行时位置:
使用edb对hello的运行过程进行解析,能够看到在运行_dl_start与_dl_init以前,GOTPLT表的内容如图所示:
5-17 GOTPLT表的内容
此时的PLT表还空空如也,由于程序尚未执行动态连接。
PLT时一个数组,PLT[0]跳转到动态连接器中,PLT[1]调用系统启动函数来初始化执行环境。直到PLT[2]开始的每一个条目才是负责具体函数的连接的。
执行完dl start后。发现GOT表中的数据发生了改变。
5-18 GOT表中的数据发生了改变
GOT[1]= 0x00007f3198405170 指向重定位条目
GOT[2]= 0x00007f31981f3750 指向动态连接器
GOT[1]所指向的重定位表以下:
5-19 GOT[1]所指向的重定位表
GOT[2]指向的动态连接器以下所示:
5-20 GOT[2]指向的动态连接器
当程序须要调用一个动态连接库内定义的函数时(例如printf),call指令并无让控制流直接跳转到对应的函数中去,因为延迟绑定的机制,此时的printf还不知道在哪儿呢。取而代之的是,控制流会跳转到该函数对应的PLT表中,而后经过PLT表将当前将要调用的函数的序号压入栈中,下一步,调用动态连接器。
接下来,动态连接器会根据栈中的信息忠实的执行重定位,将真实的printf的运行时地址写入GOT表,取代了GOT原先用来跳转到PLT的地址,变为了真正的函数地址。
因而,上一次控制流找过来时,GOT给它指的路是动态连接器,动态连接器将真正的地址给GOT表。
这一次控制流再找上门来的时候,GOT就能够放心的将真正的函数执行时地址传达过去,完成了动态连接的过程。
分析hello程序的动态连接项目,经过edb调试,分析在dl_init先后,这些项目的内容变化。要截图标识说明。
终于完成了全部的部件,组装跑车的过程老是激动人心的。但一样不容懈怠,哪怕有一丝疏忽,有一个零件装错了,在跑车高速运转的时候都会出现难以估计的灾难。
连接器在这里经过可重定位目标文件中的数据结构,解析每一个文件中的符号,仔细比对了符号的定义和引用,最终为每一个符号的引用都找到了正确的符号定义的位置。重定位的过程须要更加当心谨慎,连接器须要在特定的位置修改值,使得程序在运行时可以指哪打哪而不会误差。毕竟在cpu中哪怕是一个字节的误差,失之毫厘,差之千里。
(第5章1分)
进程是计算机科学中最深入、最成功的概念之一。
当hello程序在计算机中开始执行时,操做系统给了它一种假象,仿佛它是当前系统中惟一正在运行的程序同样,它独自占有一块完整的内存空间,cpu对它指令有求必应,处理器仿佛一直在执行hello这一个程序的指令。
这种状态就成为进程。
进程就是一个执行中的程序的实例,系统中每个程序都运行在某个进程的上下文中,系统始终维护着这个上下文,使进程与上下文之间的互动完美无缺。在操做系统的辛苦维持下,才给予了程序独自占用全部计算资源的假象。
进程提供给应用程序的关键抽象以下:
shell是一个交互型的应用级程序,它表明用户运行其余程序。
shell首先打印一个命令行提示符,等待用户输入命令行,而后对命令行进行求值。shell的基本流程是读取命令行,解析命令行,而后表明用户运行程序。
shell首先调用parseline函数,经过这个函数解析以空格分隔的命令行参数,并构造最终会传递给execve的argv向量。
若第一个参数是内置的shell命令名,立刻就会解释这个命令。若是不是,shell就会假定这是一个可执行程序,而后在一个新的子进程的上下文中加载并运行这个文件。
若最后一个参数是&,那么这个程序将会在后台执行,即shell不会等待其完成。
若没有,则这是一个将要在前台执行的程序,shell会显式地等待这个程序执行完成。
看成业终止时,shell就会开始下一轮迭代。
Hello的执行是经过在终端中输入./Hello来完成的。
在linux系统下的终端中始终运行着一个Shell来执行用户输入的操做,做为用户与系统之间的媒介。
当咱们在终端中输入./Hello时,shell会先判断发现这个参数并非Shell内置的命令,因而久把这条命令看成一个可执行程序的名字,它的判断显然是对的。
接下了shell会执行fork函数。
fork函数的做用是建立一个与当前进程平行运行的子进程。系统会将父进程的上下文,包括代码,数据段,堆,共享库以及用户栈,甚至于父进程打开的文件的描述符,都建立一份副本。而后利用这个副本执行子进程。从这个角度上来讲,子进程的程序内容与父进程是彻底相同的。
在父进程fork后,父进程重拾本身的老本行,继续运行shell的程序,而子进程将经过execve加载用户输入的程序。因为Hello是前台运行的,因此shell会显式的等待hello运行结束。
execve函数加载并运行可执行目标文件,且带参数列表argv和环境变量envp。只有当出现错误时,execve才会返回到调用程序,不然execve调用一次而从不返回。
execve的参数列表以下图:
6-1 环境变量列表的组织结构
在execve加载了Hello以后,它会调用系统提供的启动代码,启动代码设置栈,启动程序运行初始化代码。系统会用execve构建的数据结构覆盖其上下文,替换成Hello的上下文,而后将控制传递给新程序的主函数。
execve只是简单的更换了本身所处进程的上下文,并无改变进程的pid,也没有改变进程的父子归属关系。
对于正在运行的Hello来讲,除了本身的父进程是Shell以外,其它的一切都与调度运行没有区别。
在Hello进程执行的时候,操做系统为其维持着上下文。Hello进程就是在其上下文中稳定运行的。
上下文是内核从新启动一个被抢占的进程所需的状态,它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各类内核数据结构,好比描述地址空间的页表,包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。
Hello进程在内存中执行的过程当中,并非一直占用着cpu的资源。由于当内核表明用户执行系统调用时,可能会发生上下文切换,好比说Hello中的sleep语句执行时,或者当Hello进程以及运行足够久了的时候。每到这时,内核中的调度器就会执行上下文切换,将当前的上下文信息保存到内核中,恢复某个先前被抢占的进程的上下文,而后将控制传递给这个新恢复的进程。
6-2 进程上下文切换的剖析
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
在Hello运行的过程当中会屡次出现异常,经过linux系统的信号机制来使Hello正常运行。
首先,即便是Hello正常运行的时候,也会出现异常控制流。好比说,全部的系统都有某种周期性定时器中断的机制,一般为1毫秒或每10毫秒,当每次发生定时器中断时,内核就能断定当前进程已经运行了足够长的时间,这时就会调度运行另外一个进程,而将当前Hello进程搁置。
事实上的linux系统时至关复杂和繁忙的,即便开了电脑后什么也不作,系统也在后台不断地运行着几千个进程。经过调度器的调度使这些进程井井有理的使用cpu资源。
咱们须要重点讨论的是Hello函数自己的异常。
6-3 hello程序源代码
Hello程序的main函数如上图,其中的sleep函数就会像进程自己发送一个STPSIG使其休眠一段时间。在程序中,这个时间是2.5秒。
当请求的时间到了,或者sleep函数被一个信号中断,进程就会继续执行,继续调用printf函数。
当程序正常执行直到结束时,显示以下:
6-4
若是在程序运行到中途时按下ctrl+z,产生状况以下:
6-5
因为键盘输入的ctrl+z给程序传入了一个SIGSTP信号,这个信号使程序暂时挂起。此时能够输入ps命令查看进程。
6-6
能够看到,此时hello-ld程序仍然在后台进程当中而没有停止。
此时若是继续输入fg,就能使hello-ld程序继续执行。以下图
6-7
若是在程序运行的时候键入ctrl+c,就会给进程发送一个终止信号。以下图
6-8
能够看到,此时hello-ld已经不在做业列表当中了。
若是在程序执行时乱按键盘,程序仍然会正常执行:
6-9
在程序执行到一半的时候将其中止,输入pstree,可以看到当前计算机正在执行的全部进程的关系:
6-10
hello执行过程当中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程当中能够按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后能够运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
有了跑车还不算完成任务,由于如何驾驶跑车也是一个大问题,就算是老司机也不免翻车,进程管理就是为了约束程序的运行而存在的。
程序从加载的内存中开始就独自享有一份上下文,在本身的进程里自由的运行。可是为了可以有效的管理进程,系统中有称为异常的机制,可以改变控制流,使程序在本身的进程出现问题时不会一筹莫展,而是得到来自外部的帮助。一样的,不一样进程之间须要沟通,信号就是为此而存在的。信号时管理程序运行的一大利器。
(第6章1分)
机器语言指令中出现的内存地址,都是逻辑地址,须要转换成线性地址,再通过MMU(CPU中的内存管理单元)转换成物理地址才可以被访问到。
逻辑地址:包含在机器语言中用来指定一个操做数或一条指令的地址。每个逻辑地址都由一个段(segment)和偏移量(offset)组成,偏移量指明了从段开始的地方到实际地址之间的距离。
通俗的说:逻辑地址是给程序员设定的,底层代码是分段式的,代码段、数据段、每一个段最开始的位置为段基址,放在如CS、DS这样的段寄存器中,再加上偏移,这样构成一个完整的地址。
Linux中逻辑地址等于线性地址。为何这么说呢?由于Linux全部的段(用户代码段、用户数据段、内核代码段、内核数据段)的线性地址都是从 0x00000000 开始,长度4G,这样 线性地址=逻辑地址+ 0x00000000,也就是说逻辑地址等于线性地址了。
虚拟地址将贮存当作是一个存储在磁盘上的地址空间的高速缓存,再主存中只保存活动区域,并根据须要再磁盘和主存之间来回传送数据,经过这种方式,它高效的使用了主存。同时,它为每一个进程提供了一致的地址空间,从而简化了内存管理。最后,它保护了每一个进程的地址空间不被其余进程破坏。
而物理地址则是对应于主存的真实地址,是可以用来直接在主存上进行寻址的地址。因为在系统运行时,主存被不一样的进程不断使用,分区状况很复杂,因此若是要用物理地址直接访问的话,地址的处理会至关麻烦。
在多段模式下,每一个程序都有本身的局部段描述符表,而每一个段都有独立的地址空间
在80386 的段机制中,逻辑地址由两部分组成,即段部分(选择符)及偏移部分。
段是造成逻辑地址到线性地址转换的基础。若是咱们把段当作一个对象的话,那么对它的描述以下。
(1)段的基地址(Base Address):在线性地址空间中段的起始地址。
(2)段的界限(Limit):表示在逻辑地址中,段内可使用的最大偏移量。
(3)段的属性(Attribute): 表示段的特性。例如,该段是否可被读出或写入,或者该段是否做为一个程序来执行,以及段的特权级等。
7-1
分页管理是地址翻译的一个基本思路。
概念上而言,虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。每字节都有一个惟一的虚拟地址做为到数组的索引。磁盘上数组的内容被缓存在主存中。和存储器层次结构中其余缓存同样,磁盘(较低层)上的数据被分割成块,这些块做为自盘和主存(较高层)之间的传输单元。VM系统经过将虚拟内存分割为成为虚拟页的大小固定的块来处理这个问题,对这些虚拟页的管理与调度就是页式管理。
同任何缓存同样,虚拟内存系统必须有某种方法来断定一个虚拟页是否缓存在DRAM中的某个地方。若是是,系统还必须肯定这个虚拟页存放在哪一个物理页中。若是不命中,系统必须判断这个虚拟页存放在磁盘的那个位置,在物理内存中选择一个牺牲页,并将虚拟页从磁盘复制到DRAM中,替换这个牺牲页。
首先讨论单级页表下的VA到PA的变换。
当一个进程执行一条访存指令时,它发出的内存地址是虚拟地址,由内存管理单元(Memory Management Unit MMU)将虚拟地址转化为物理地址,并访问主存,取出所要读取的数据。
页表的地址映射规则以下:
7-2 使用页表的地址翻译
在这个过程当中,cpu硬件将会执行如下步骤:
7-3 页命中和缺页的操做图
不一样存储技术的访问时间差别很大,速度较快的计数每字节的成本要比速度较慢的计数高,并且容量较小。计算的另外一个特色就是局部性,即计算机程序倾向于访问最近访问过的某一块程序。存储器的这些基本属性相互补充使得计算机能够经过采用构建存储器层次结构来提高运行效率。
三级Cache的核心思想就是每次访问数据的时候都将一个数据块存放到更高一层的存储器中,根据计算的局部性,程序在后面的运行之中有很大的几率再次访问这些数据,高速缓存器就可以提升读取数据的速度。
7-4
当fork函数被当前进程调用的时候,内核会为新进程建立各类数据结构,并分配给它一个惟一的PID。为了给这个新进场建立虚拟内存,它建立了当前进程的mm_struct、区域结构和页表的原样副本,它将两个进程中的每一个页面都标记为只读,并将两个进程中的每一个区域结构都标记位私有的写时复制。
7-5 一个私有的写时复制对象
虚拟内存的机制使得fork函数能够快速的运行,由于当咱们fork了一个新进程的时候,系统事实上并无将原进程的整个上下文复制一遍,它仅仅只是建立了份如出一辙的描述地址空间的数据结构,而后将这个数据结构给予子进程。当子进程执行只读代码时,它与父进程实际上共用了物理内存中的同一片区域的内容。
当fork在新进程中返回时,新进场如今的虚拟内存恰好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操做时,写时复制机制就会建立信也米娜。所以,经过虚拟内存这种巧妙的机制为每一个进程都保持了私有地址空间的抽象概念。
同理,在虚拟内存的机制下,execve也能够简单快速的实现。
经过execve函数在当前进程中加载并运行包含之可执行目标文件中的程序,用a.out程序有效地替代了当前程序。这个过程有如下几个步骤:
删除当前进程虚拟地址的用户部分中已存在的区域结构。
为新程序的代码、数据、bss和栈区域建立新的区域结构。全部这些新的区域都是私有的,写时复制的。代码和数据区域被映射为a.out文件中的.test和.data区。bss区域是请求二进制0的,映射到匿名文件,其大小包含在a.out当中。栈和堆区域也是请求二进制零的,初始长度为零。下图归纳了私有区域的不一样映射。
7-6 加载器是如何映射用户地址空间区域
若是a.out程序与共享对象(或目标)连接,好比标准C库libc.so,那么这些对象都是动态连接到这个程序的,而后再映射到用户虚拟地址空间中的共享区域内。
execve作的最后一件事情就是设置当前进程上下文中的程序计数器,食指指向代码区域的入口点。
同任何缓存同样,虚拟内存系统必须有某种方法来断定一个虚拟页是否缓存在DRAM中的某个地方。若是是,系统还必须肯定这个虚拟页存放在哪一个物理页中。若是不命中,系统必须判断这个虚拟页存放在磁盘的那个位置,在物理内存中选择一个牺牲页,并将虚拟页从磁盘复制到DRAM中,替换这个牺牲页。
当CPU想要读取虚拟内存中的某个数据,而这一片数据刚好存放在主存当中时,就称为页命中。相对的,若是DRAM缓存不命中,则称之为缺页。若是CPU尝试读取一片内存而这片内存并无缓存在主存当中时,就会触发一个缺页异常,这个异常的类型是故障。此时控制流转到内核中,由内核来尝试解决这个问题。
7-7 触发缺页
缺页异常会调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,而后用磁盘中将要读取的页来替代牺牲页。处理程序解决了这个故障,将控制流转移会原先触发缺页故障的指令,当cpu再次执行这条指令时,对应的页已经缓存到主存当中了。这就是缺页故障与缺页中断的处理。
7-8 解决缺页异常
动态内存分配器经过维护一个存放着堆的分配状况的数据结构来实现动态的内存分配。
mem_init函数将对于堆来讲可用的虚拟内存模型化为一个大的,双字对齐的字节数组。在mem_heap和mem_brk之间的字节表示已分配的虚拟内存。mem_brk以后的字节表示未分配的虚拟内存。分配器经过调用mem_sbrk函数来请求额外的堆内存。
分配器须要知足下列要求
处理任意请求序列
当即响应请求:分配器必须当即响应请求。所以,不容许分配器为了提升性能重行排列或者缓冲请求。
只使用堆:为了使分配器能够拓展,分配器使用的任何非标量数据结构都要保存到堆里。
对齐块:使得其能够保存任何类型的数据对象。
不修改已经分配的块。
隐式空闲链表分配中,内存块的基本结构以下:
7-9 使用边界标记的堆块的格式
其中头部和脚部分别存放了当前内存块的大小与是否已分配的信息。
经过这种结构,隐式动态内存分配器会对堆进行扫描,经过上图中的头部和脚部的结构来实现查找。
显式空间链表的一种实现的基本结构以下:
7-10 空闲块
将一个空闲内存块的有效载荷利用起来,存放着指向下一个以及上一个空闲块的指针。
经过这种结构能够实现将内存块以不按顺序的形式组织成合适的结构,好比说递增序列。一般会在初始化堆的时候额外开辟一块对空间,用于存放用来维护链表的数据结构。
马路上不可能只有一辆车,所以车行的前后,车辆的避让须要一套规则来管理。所以开车光是掌握了车辆的驾驶技术还不行,还须要交通规则。交通规则能够类比为计算机中的虚拟内存机制,管理着存储资源的调度。
为了更加有效地管理内存而且少出错,现代系统提供了一种对主存的抽象概念,叫作虚拟内存(VM)。虚拟内存是硬件异常、硬件地址翻译、主存、磁盘文件和内核软件的完美交互,它为每一个进程提供了一个大的,一致的和私有的地址空间。经过一个很清晰的机制,虚拟内存提供了三个重要的能力:
(第7章 2分)
一个Linux文件就是一个m个字节的序列,全部的I/O设备(例如网络,磁盘和终端)都被模型化为文件,而全部的输入和输出都被看成对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,容许Linux内核引出一个简单的、低级的应用皆可,称为Unix I/O,这使得全部的输入和输出都能以一种统一且一致的方式来执行:
设备的模型化:文件
设备管理:unix io接口
相似的,写操做就是从内存复制n>0个字节到一个文件,从当前文件位置k开始,而后更新k。
unix io 函数(须要包含头文件 <sys/types.h><sys/stat.h><fcntl.h>):
open函数将filename转换为一个文件描述符,而且返回描述符数字,返回的描述符老是在进程中当前没有打开的最小描述符。flags参数指明了进程打算如何访问这个文件;
只读
只写
可读可写
若是文件不存在,就建立它的一个截断的文件
若是文件已存在,就截断它
在每次写操做前,设置文件位置到文件的结尾处
关闭一个打开的文件
从描述符为fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,而返回值0表示EOF。不然,返回值表示实际传送的字节数量
从内存位置buf复制最多n个字节到描述符fd的当前文件位置。
谈起printf的具体实现,首先看看printf函数的函数体:
8-1 printf函数体
注意到函数体的参数列表里面有一个”…”,这个符号表达的意思是参数的个数不肯定。那么printf函数所要作的第一件事,就是确认函数的参数到底有多少。
注意到函数体中有这样一条定义:
而由函数的栈帧结构:
8-2 函数的栈帧结构
能够推断出,arg指针指向了传递给printf的第一个参数的地址。
接下来函数调用了vsprintf函数,其函数体以下:
8-3 vsprintf函数体
阅读函数体能够知道,这个函数的做用就是格式化。它接受肯定输出格式的格式字符串fmt,用格式字符串堆个数变化的参数进行格式化,产生格式化输出。
接下来,printf函数会调用系统io函数:write
write是一个系统函数,其做用就是从内存buf位置复制最多i个字节到一个文件位置。而在linux系统中,系统IO被抽象为文件,包括屏幕。对于系统来讲,咱们的显示屏也是一个文件,咱们只须要将数据传送到显示屏对应的文件,就已经完成了系统端的任务,余下的工做独立的由显示器来进行了。
因而在这里,write会给寄存器传递几个参数,初始化执行环境,而后执行sys call指令,这条指令的做用是产生陷阱异常。
陷阱是有意的异常,用户程序执行了系统调用的命令(syscall)以后,就致使了一个到异常处理程序的陷阱,这个处理程序解析参数,并调用适当的内核程序。
8-4 陷阱处理
须要注意,这里的系统调用试运行在内核模式中的。
接下来,系统已经肯定了所要显示在屏幕上的符号。根据每一个符号所对应的ascii码,系统会从字模库中提取出每一个符号的vram信息。
显卡使用的内存分为两部分,一部分是显卡自带的显存称为VRAM内存,另一部分是系统主存称为GTT内存(graphics translation table和后面的GART含义相同,都是指显卡的页表,GTT 内存能够就理解为须要创建GPU页表的显存)。在嵌入式系统或者集成显卡上,显卡一般是不自带显存的,而是彻底使用系统内存。一般显卡上的显存访存速度数倍于系统内存,于是许多数据若是是放在显卡自带显存上,其速度将明显高于使用系统内存的状况(好比纹理,OpenGL中分普通纹理和常驻纹理)。
显示芯片按照刷新频率逐行读取vram,并经过信号线向液晶显示器传输每个点(RGB份量)。
getchar 由宏实现:#define getchar() getc(stdin)。
getchar 有一个int型的返回值.当程序调用getchar时.程序就等着用户按键.用户输入的字符被存放在键盘缓冲区中.直到用户按回车为止(回车字符也放在缓 冲区中).当用户键入回车以后,getchar才开始从stdin流中每次读入一个字符.getchar函数的返回值是用户输入的第一个字符的ASCII 码,如出错返回-1,且将用户输入的字符回显到屏幕.如用户在按回车以前输入了不止一个字符,其余字符会保留在键盘缓存区中,等待后续getchar调用 读取.也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为后,才等待用户按键.
getchar函数的功能是从键盘上输入一个字符。其通常形式为: getchar(); 一般把输入的字符赋予一个字符变量,构成赋值语句。
进入getchar以后,进程会进入阻塞状态,等待外界的输入。系统开始检测键盘的输入。此时若是按下一个键,就会产生一个异步中断,这个中断会使系统回到当前的getchar进程,而后根据按下的按键,转化成对应的ascii码,保存到系统的键盘缓冲区。
接下来,getchar调用了read函数。read函数会产生一个陷阱,经过系统调用,将键盘缓冲区中存储的刚刚按下的按键信息读到回车符,而后返回整个字符串。
接下来getchar会对这个字符串进行处理,只取其中第一个字符,将其他输入简单的丢弃,而后将字符做为返回值,并结束getchar的短暂一辈子。
IO是复杂的计算机内部与外部沟通的通道。尽管咱们时时刻刻都在使用着IO:经过键盘输入,经过屏幕阅读。可是系统IO实现的细节一样也是至关复杂的。
本章介绍了linux系统下的IO的基本知识,讨论了IO在linux系统中的形式以及实现的模式。而后对printf和getchar两个函数的实现进行了深刻的探究。
(第8章1分)
首先经过键盘向计算机输入一串代码,这串代码组合成了一个hello.c源文件。
接下来将源文件经过gcc编译器预处理,编译,汇编,连接,最终完成一个能够加载到内存执行的可执行目标文件。
接下来经过shell输入文件名,shell经过fork建立一个新的进程,而后在子进程里经过execve函数将hello程序加载到内存。虚拟内存机制经过mmap为hello规划了一片空间,调度器为hello规划进程执行的时间片,使其可以与其余进程合理利用cpu与内存的资源。
而后,cpu一条一条的从hello的.text取指令执行,不断从.data段去除数据。异常处理程序监视着键盘的输入。hello里面的一条syscall系统调用语句使进程触发陷阱,内核接手了进程,而后执行write函数,将一串字符传递给屏幕io的映射文件。
文件对传入数据进行分析,读取vram,而后在屏幕上将字符显示出来。
最后程序运行结束,shell将进程回收,完成了hello程序执行的全过程
从键盘上敲出hello.c的源代码程序不过几分钟,从编译到运行,从敲下gcc到终端打印出hello信息,可能甚至不须要1秒钟。
这短短的1秒,聚集了计算机工做者们几十年的智慧与心血。
高低电平传递着信息,这些信息被复杂而严谨的机器逻辑捕捉。cpu不知疲倦的取指与执行。对于hello的实现细节,哪怕把这篇论文再扩充一倍仍讲不清楚。正由于如此,我意识到本身还有很长的路要走。
(结论0分,缺失 -1分,根据内容酌情加分)
Hello.c |
Hello的c源代码 |
Hello.i |
源代码预编译产生的ascii文件 |
Hello.s |
Ascii文件编译后产生的汇编代码文件 |
Hello.o |
汇编后产生的可重定位目标文件 |
Hello_o-objdump-d.txt |
可重定位目标文件的对应汇编代码 |
Hello_o-objdump-d-r.txt |
可重定位目标文件的代码及重定位条目 |
Hello_o-readelf-a.txt |
可重定位目标文件的elf条目 |
Hello-ld |
连接生成的可执行目标文件 |
Hello-ld-readelf-a.txt |
可执行目标文件对应的elf条目 |
Hello-objdump-d-r.txt |
可执行目标文件对应的汇编代码 |
Hello-linkinfo.txt |
可执行目标文件的连接信息 |
列出全部的中间产物的文件名,并予以说明起做用。
(附件0分,缺失 -1分)
为完成本次大做业你翻阅的书籍与网站等
[1]Eteran, Evan. “Eteran/Edb-Debugger.” GitHub, 2018, github.com/eteran/edb-debugger/wiki/Data-View.
[2]flood, rain. “编译并链接从helloworld.c生成的汇编代码的方法步骤.” 为何版本控制如此重要? - CSDN博客, 2017, blog.csdn.net/rainflood/article/details/75635447.
[3]stx, piani. “Pianistx.” 数字故宫(360全景+纪录片+数据库+公开课) - Zeroassetsor - 博客园, 2014, www.cnblogs.com/pianist/p/3315801.html.
[4]toeic, clover. “clover_toeic.” 数字故宫(360全景+纪录片+数据库+公开课) - Zeroassetsor - 博客园, 2014, www.cnblogs.com/clover-toeic/p/3851102.html.
[5]Xu, Mike. “Linux中的逻辑地址,线性地址和物理地址转换关系.” 为何版本控制如此重要? - CSDN博客, 2014, blog.csdn.net/u011253734/article/details/41173849.
(参考文献0分,缺失 -1分)