该报告从细节出发,讲述了hello.c从c文件一步步成功运行的绝大多数过程。预处理、编译、汇编、链接,每一步的操作如何对上一步的文件进行操作,形成新的文件。hello进程在shell执行的过程,存储管理的过程,IO处理的过程也有较为详细的介绍解释。
关键词:程序的生命周期;hello;从程序到进程;进程的管理;
第1章 概述
1.1 HELLO简介
1.2 环境与工具
1.3 中间结果
1.4 本章小结
第2章 预处理
2.1 预处理的概念与作用
2.2在UBUNTU下预处理的命令
2.3 HELLO的预处理结果解析
2.4 本章小结
第3章 编译
3.1 编译的概念与作用
3.2 在UBUNTU下编译的命令
3.3 HELLO的编译结果解析
3.4 本章小结
第4章 汇编
4.1 汇编的概念与作用
4.2 在UBUNTU下汇编的命令
4.3 可重定位目标ELF格式
4.4 HELLO.O的结果解析
4.5 本章小结
第5章 链接
5.1 链接的概念与作用
5.2 在UBUNTU下链接的命令
5.3 可执行目标文件HELLO的格式
5.4 HELLO的虚拟地址空间
5.5 链接的重定位过程分析
5.6 HELLO的执行流程
5.7 HELLO的动态链接分析
5.8 本章小结
第6章 HELLO进程管理
6.1 进程的概念与作用
6.2 简述壳SHELL-BASH的作用与处理流程
6.3 HELLO的FORK进程创建过程
6.4 HELLO的EXECVE过程
6.5 HELLO的进程执行
6.6 HELLO的异常与信号处理
6.7本章小结
第7章 HELLO的存储管理
7.1 HELLO的存储器地址空间
7.2 INTEL逻辑地址到线性地址的变换-段式管理
7.3 HELLO的线性地址到物理地址的变换-页式管理
7.4 TLB与四级页表支持下的VA到PA的变换
7.5 三级CACHE支持下的物理内存访问
7.6 HELLO进程FORK时的内存映射
7.7 HELLO进程EXECVE时的内存映射
7.8 缺页故障与缺页中断处理
7.9动态存储分配管理
7.10本章小结
第8章 HELLO的IO管理
8.1 LINUX的IO设备管理方法
8.2 简述UNIX IO接口及其函数
8.3 PRINTF的实现分析
8.4 GETCHAR的实现分析
8.5本章小结
结论
附件
参考文献
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
p2p(From program to process):hello刚开始由程序员通过外部输入设备键盘输入并保存为.c文件,这时hello成为了一个program。然后通过预处理器(preprocessor)处理#开始的预编译指令,如宏定义(#define)、文件包含(#include)、条件编译(#ifdef)等。接着进行编译,使用编译器(compiler)将C语言,翻译汇编代码。再通过汇编器(Assembler)将汇编代码翻译成二进制机器语言,最后使用连接器(Linker)将汇编器生成的目标文件外加库链接为一个可执行文件。
020:shell将fork一个子进程然后execve hello进程,映射虚拟内存,使用TLB,4级页表、3级Cache找到对应物理内存,然后内核调度分配时间片。结束时,shell父进程回收子进程。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
硬件:inter core i5-7300HQ x86-64,8G RAM,128G SSD +1T Disk;
软件:Windows 10 64位;Vmware14 pro;ubuntu-18.04.1 64位;
开发与调试工具:GCC,GDB, ld, readelf, edb.
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
文件名称 | 作用 |
---|---|
hello.i | 预处理之后的文件 |
hello.s | 编译之后的汇编文件 |
hello.o | 汇编之后的可重定位目标文件 |
hello | 链接之后的可执行目标文件 |
helloobjdump.txt | hello.o的反汇编文件 |
helloobjdump2.txt | hello的反汇编文件 |
hello.elf | hello.o的ELF头 |
hello2.elf | hello的ELF头 |
helloreadelf.txt | hello.o的ELF所有信息 |
helloreadelf2.txt | hello的ELF所有信息 |
1.4 本章小结
这一章只是这个实验的开头,简介一些实验基本信息,hello简介,环境,中间产物。
2.1 预处理的概念与作用
概念:预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。由预处理器(preprocessor) 对程序源代码文本进行处理,得到的结果再由编译器核心进一步编译。这个过程并不对程序的源代码进行解析,但它把源代码分割或处理成为特定的单位,预处理记号(preprocessing token)用来支持语言特性。
作用:根据源代码中的预处理指令修改你的源代码。预处理指令是一种命令语句(如#define),它指示预处理程序如何修改源代码。在对程序进行通常的编译处理之前,编译程序会自动运行预处理程序,对程序进行编译预处理,这部分工作对程序员来说是不可见的。
预处理程序读入所有包含的文件以及待编译的源代码,然后生成源代码的预处理版本。在预处理版本中,宏和常量标识符已全部被相应的代码和值替换掉了。如果源代码中包含条件预处理指令(如#if),那么预处理程序将先判断条件,再相应地修改源代码。
2.2在Ubuntu下预处理的命令
命令:gcc -E hello.c -o hello.i
截图:
图2.2 预处理命令
2.3 Hello的预处理结果解析
结果得到一个hello.i文件,我们将其打开观察。
图2.3.1和图2.3.2 hello.i部分截图和原.c程序截图
由于我们的c程序中没有宏定义 (#define)、条件编译(#ifdef)所以在预处理之后,我们在预处理后的文件的3100行发现自己的程序主题部分与原.c文件无任何变化,但是我的原.c文件的头文件(#include)已经在被处理了,所以在此处找不到原头文件。
2.4 本章小结
本章简要的介绍了预处理的概念与作用,预处理就是在正式编译之前做了一些简单的工作(替换宏定义等),然后使用#gcc -E hello.c -o hello.i将.c文件处理为.i文件,观察.i文件我们知道了预处理后的程序主体会在.i文件的最后出现。
3.1 编译的概念与作用
概念:将某一种程序设计语言写的程序(这里是指预处理之后.s的程序)翻译成等价的另一种语言的程序的程序(汇编之前.s文件的程序), 称之为编译
作用:1.词法分析,2.语法分析,3.语义分析,4.源代码优化(中间语言生成),5.代码生成,目标代码优化。
3.2 在Ubuntu下编译的命令
命令: gcc -s hello.c -o hello.s
截图:
3.3 Hello的编译结果解析
3.3.1数据
a.全局变量sleepsecs:
图3.3.1.a sleepsecs的声明
全局变量sleepsecs在第8行声明,而其在.c中为被声明为初始化的全局变量,所以编译之后将其存储在.data里。
b.局部变量i:
图3.3.1.b 局部变量i
局部变量i第一次在程序中出现在37行处,因为在.c源文件中i被定义为局部变量,将其存储在用户栈中或直接存储在寄存器中。但是37行的操作告知我们i变量在-4(%rbp)位置中,即在用户栈里。
c.常量:
直接用立即数表示。
d. 字符串常量:
图3.3.1.d 字符串常量
在两个printf中的字符.LC0和.LC1在12-15行声明,字符串存储在.rodata节中。
3.3.2 赋值
a.sleepsecs
图3.3.2.a sleepsecs的声明
sleepsecs为已初始化的全局变量,在声明时直接赋值为2。
b.i
图3.3.2.b 局部变量i
i为未初始化的局部变量,其存储在用户栈中,赋值直接使用mov指令即可。
3.3.3类型转换
a.隐式转换
图3.3.3.a sleepsecs的声明
.c源程序中sleepsecs的声明为 int sleepsecs=2.5;由于sleepsecs的类型定义为int,所以将其赋值为浮点型的2.5会被隐式转换(Round to 0)为int型的2,所以在声明是直接将其值赋位2。
3.3.4 算数操作
a.++
图3.3.4.a i++
++算数操作在.s文件的53行被翻译为add操作(源操作数为1)
3.3.5 关系操作
a.!=
图3.3.5.a !=操作
.c源程序中的i!=3,在编译时被翻译为cmp指令,cmp $3, -20(%rbp),
将-20(%rbp)中存储的i与3比较,并设置条件码,je指令根据条件码决定下一步操作。
b.<
图3.3.5.b <操作
.c源文件中的语句i<10,在编译时使用cmp指令设置条件码,并设置条件码,jle指令根据条件码决定下一步操作,实现<关系操作。
3.3.6 数组/指针/结构操作
a.数组操作
图3.3.6.a1 源文件中数组操作
图3.3.6.a2 数组准备
图3.3.6.a3 数组操作
.c源程序中的数组操作出现在循环体for循环中,每次循环中都要访问argv[1],argv[2]这两个内存。在翻译时,argv[]先是被存在用户栈中(29行),再使用基址加偏移量寻址访问argv[1],argv[2]。
argv[1]: 数组首地址存放于-32(%rbp)(29行),现将其存储到%rax中(43行),在加上偏移量$8(44行),再将该位置内容放在%rsi中(46行)成为下一个函数的第二个参数。
argv[2]: 数组首地址存放于-32(%rbp)(29行),现将其存储到%rax中(40行),在加上偏移量$16(41行),再将该位置内容放在%rdi中(42行)成为下一个函数的第一个参数。
3.3.7 控制转移
a.if语句
图3.3.7.a1 源文件里的if语句
图3.3.7.a2 if语句
源文件里的的if语句判断条件是argc!=3,在翻译后变为cmp语句,
cmp $3,(%rbp), cmp语句只是设置了条件码,而控制转移则还需接下来的je语句一同执行,当相等时跳转到.L2处,不相等时顺序执行,实现控制转移。
b.for语句
图3.3.7.b1 源文件里的for语句
图3.3.7.b2 for语句
源程序的for语句主要三部分:初始化、循环条件、循环变量改变,首先初始化在翻译时对应了在37行的mov操作,将i=0;接着38行使用了for循环翻译的一种常见方法跳转到中间;循环条件在55行处翻译为cmp语句,将i与9比较并设置条件码,56行根据条件码来实现控制转移,39-52行时循环体内部的指令暂且不谈,循环变量改变在53行翻译为add操作,每次使i+1;
3.3.8 函数操作
函数操作包含传递参数,函数调用,函数返回(return)
a. main函数
传递参数:在程序被执行时,_start从由内核设置的栈中获取参数和环境变量信息,然后调用 main函数将参数argc和avgv[]传递到main函数。
函数调用: 可执行文件的开始地址是_start的地址,控制传递给_start以后,然后调用__libc_start_main(__libc_start_main被认为main)。
函数返回:main函数使用return返回0,翻译为movl $0 , %eax; ret; 先将%eax置成0,在使用ret返回。
b.printf函数
传递参数:第一次调用将.L0放置在%rdi传递给函数puts;第二次调用将.L1放置在%rdi中传递,将argv[1]放在%rsi中传递,将argv[2]放在%rdx中传递。
函数调用:第一次在main函数中直接call puts调用,第二次main在函数中调用
函数返回:返回打印的字符数量(在本程序中没有使用)
c.exit函数
传递参数:使用mov,将%edi置成一,传入exit函数中。
函数调用:在main函数里直接call exit函数。
d.sleep函数
传递参数:由于数据sleepsecs是已初始化的全局变量,在传递参数时将sleepsecs传递给%edi即可。
函数调用:在main函数里直接call sleep函数。
e.getchar函数
函数调用:在main函数里直接call getchar 函数。
3.4 本章小结
本章显示介绍了有关编译的概念作用,然后使用#gcc -s hello.c -o hello.s生成了
编译后的文件。对于生成的.s文件,我们讲解了C语言的数据与操作在机器之中如何被处理翻译的,这为我们的下一步汇编打下了继续的基础。
4.1 汇编的概念与作用
概念:把汇编语言翻译成机器语言的过程称为汇编。在汇编语言中,用助记符(Memoni)代替操作码,用地址符号(Symbol)或标号(Label)代替地址码。这样用符号代替机器语言的二进制码,就把机器语言变成了汇编语言。
作用: 用汇编语言编写的程序,机器不能直接识别,要由一种程序将汇编语言翻译成机器语言,汇编程序起这种翻译作用。
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
4.2 在Ubuntu下汇编的命令
命令 gcc -c hello.s -o hello.o
截图:
图4.2 汇编截图
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
图4.3.1 ELF头
图4.3.2 节头部表
图4.3.3 重定位信息
重定位是将EFL文件中的未定义符号关联到有效值的处理过程。在main.o中,这意味着对printf,exit等函数的未定义的引用和全局变量(sleepsecs)必须替换为该进程的虚拟地址空间中适当的机器代码所在的地址。在目标中用到的相关符号之处,都必须替换。
4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。
hello的反汇编与hello.s的汇编不同之处:
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
图5.2 ld链接命令
5.3 可执行目标文件hello的格式
图5.3.1 hello的ELF头
图5.3.2 hello的节头表
hello的ELF头和hello.o的ELF头大体一致,但是类型REL (可重定位文件)变为了EXEC (可执行文件),程序头从无到有,节头和字符串表索引节头的数量变多。
节头表中给出了hello各段的基本信息,括各段的起始地址,大小等
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
图5.4 hello节头表与虚拟地址空间各段
5.5 链接的重定位过程分析
hello的反汇编与hello.o的反汇编的不同:
1. 多出若干节
原hello.o反汇编只有一个.text节,而hello的反汇编则多出了.init(初始化时调用其中的_init函数),.plt(动态链接-过程链接表),.fini(程序正常终止时执行) 节。
2. 调用函数的地址确定
图5.5 hello的反汇编的exit函数
上一章我们提到hello.o反汇编之后call 后面的是下一条指令的地址,不是实际的地址。现在在hello反汇编的call 之后是函数的地址。链接过程中,动态链接器(ld-linux.so)链接程序在运行过程中根据记录的共享对象的符号定义来动态加载共享库,然后完成重定位。在此可执行文件被执行时,动态链接库的全部内容将被映射到运行时相应进程的虚地址空间。动态链接程序将根据可执行程序中记录的信息找到相应的函数代码。
3. 重定位节,符号定义与符号引用
a. 重定位节和符号定义
链接器将所有类型相同的节合并为同一类型的新的聚合节。
b.符号引用
链接器修改.data和.text节中对每个符号的引用,使得他们正确的指向运行时地址。执行这一步链接器依赖于可重定位节的重定位条目。
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
程序名 | 程序地址 |
---|---|
ld-2.27.so!_dl_start | 0x7fce 8cc38ea0 |
ld-2.27.so!_dl_init | 0x7fce 8cc47630 |
hello!_start | 0x400500 |
libc-2.27.so!__libc_start_main | 0x7fce 8c867ab0 |
-libc-2.27.so!__cxa_atexit | 0x7fce 8c889430 |
-libc-2.27.so!__libc_csu_init | 0x4005c0 |
hello!_init | 0x400488 |
libc-2.27.so!_setjmp | 0x7fce 8c884c10 |
hello!main | 0x400532 |
hello!puts@plt | 0x4004b0 |
hello!exit@plt | 0x4004e0 |
*hello!printf@plt | 无 |
*hello!sleep@plt | 无 |
*hello!getchar@plt | 无 |
ld-2.27.so!_dl_runtime_resolve_xsave | 0x7fce 8cc4e680 |
-ld-2.27.so!_dl_fixup | 0x7fce 8cc46df0 |
–ld-2.27.so!_dl_lookup_symbol_x | 0x7fce 8cc420b0 |
libc-2.27.so!exit | 0x7fce 8c889128 |
5.7 Hello的动态链接分析
动态链接使我们在调用一个共享库定义的函数可以在运行时找到函数的地址。但是在调用时编译器没办法预测这个函数(共享库定义)的运行时地址,因为定义它的共享模块可以在运行时加载到任何位置。但是GNU编译系统通过延迟绑定技术来解决这个问题,将过程地址的绑定推迟到第一次调用该过程中。
延迟绑定通过:GOT和PLT实现,如果一个目标模块调用定义在共享库中的任何函数,那么他就有自己的GOT和PLT。
第一次调用共享库函数时,不调用共享库函数,直接进入函数对应的PLT中,接着PLT指令通过对应的GOT指令进行间接跳转,由于每个GOT指令初始时都指向他对应的PLT条目的第二条指令,所以这个间接跳转只是简单的把控制传回PLT条目的下一条指令。接着把函数的ID入栈PLT跳转到PLT[0],PLT[0]再将动态链接器的一个参数入栈,然后间接跳转到动态链接器中。动态链接器依据两个栈条目确定函数的运行位置,重写对应的GOT条目,再把控制传给函数。
所以,在运行dl_init前,GOT表中存放的都是对应PLT条目的第二条指令,在运行dl_init后,GOT表中存放的就是对应的函数的地址。
(GOT位置由节头表偏移量和大小即可得知601000-601040)
图5.7.1 dl_init前后的GOT(从10-30,共四行)
5.8 本章小结
这一章我们主要介绍了链接的概念与作用,又使用ld进行链接,分析了hello的格式,节头表,各段等信息。发现偏移量与进程的虚拟地址空间各段位置一一对应。比较hello与hello.o反汇编的不同处,发现共享库函数的地址变为了实际地址,又寻找了hello从头到尾的运行的函数。最后分析了hello的动态链接终于找到了共享库函数使用延迟绑定的方法,利用PLT和GOT帮助最终找到函数的地址。
6.1 进程的概念与作用
概念:(经典定义)一个正在执行的程序的示例,是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
作用:提供给应用进程两个关键抽象1.一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器。2.一个私有的地址空间,它提供一个假象,好像我们程序在独占整个内存系统。
6.2 简述壳Shell-bash的作用与处理流程
作用:Shell俗称壳,是指“为使用者提供操作界面”的软件(命令解析器),它接收用户命令,然后调用相应的应用程序。
处理流程:
1.打印提示信息
2.等待用户输入
3.接受命令
4.解释命令
5.找到该命令,执行命令,如果命令含有参数,输入的命令解释它
6.执行完成,返回第一步
6.3 Hello的fork进程创建过程
********* **********——>hello进程 ————>exit
*******************|……………………………… |
*******************| ………………………………|
—终端———fork———————————waitpid———exit—>
图 . Hello的fork进程创建过程
在shell中键入命令./hello,shell分析输入的命令并解释执行hello程序,就使用fork创建子进程,子进程几乎与父进程完全相同,子进程得到了父进程的一个相同且独立的一份副本,但是二者之间的最大不同在于他们拥有不同的PID。
6.4 Hello的execve过程
fork创建子进程后,使用execve在当前进程中载入并运行一个新程序即hello。
execve函数加载hello并带参数列表argv和环境变量envp。它会覆盖当前进程的代码、数据、栈,但是保留PID,继承已打开的文件描述符和信号上下文。
6.5 Hello的进程执行
在hello执行之前,内核被其他进程调用,接着在某些时刻,内核可以决定抢占当前进程,并开始一个先前被抢占了的进程(如hello),hello就可以被执行。这种决策就叫做调度,是由内核中的调度器代理的。在内核调度hello后,它就抢占当前进程并使用上下文切换转移控制到hello进程。
上下文是内核重新启动一个被抢占的进程所需要的状态。它由一些对像的值组成,如寄存器,用户栈,内核栈,内核数据结构。而上下文切换就是先保存当前进程的上下文,再恢复hello进程的上下文,最后将控制转移到hello进程。
图6.5.1 sleep导致的上下文切换(A为hello)
当hello进程每次执行到sleep时,程序陷入休眠状态,内核调度其他进程,而sleep函数结束后,hello进程重新进入待执行进程队列中等待内核调度。
图6.5.2 getchar导致的上下文切换(A为hello)
内核调度后,执行到getchar函数时,调用read函数,陷入到内核。内核中的陷阱处理程序请求来自磁盘处理器的DMA传输,并且安排在磁盘控制器完成从磁盘到内存的数据传输后,磁盘中断处理器。内核调度其他进程。之后当磁盘发出一个中断信号,表示数据已经从磁盘传送到了内存。内核判断其他进程已经运行了它的足够长时间,就执行一个从其他进程到进程hello的上下文切换,将控制传递到read之后的指令,进程hello继续执行。
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
异常种类 产生信号 处理方法
中断 SIGSTP 停止直到下一个SIGCONT
终止 SIGINT 终止程序
图6.6.0 程序正常运行(最后键入换行符)
图6.6.2 运行过程中按下Ctrl-C
3. 程序在打印三行信息后,按下Ctrl-C,内核发出SIGINT信号给前台进程组中的每个进程,hello进程就被终止,且被回收。
图6.6.3 运行过程中按下若干回车
4. 在程序运行过程中,我们随意的按下若干回车,发现回车在按下时只是简单地在输出中换行而在hello进程结束后所按的回车使得shell读入几个换行。
图6.6.4 运行过程中随便乱按
5.在运行过程中我们的随便乱按没有影响程序的正常运行,在运行到getchar时,键入的一个换行结束的字符串,进程结束。
图6.6.5.1 Ctrl-Z之后运行ps命令
图 6.6.5.2 Ctrl-Z之后运行jobs命令
图6.6.5.3 Ctrl-Z之后运行pstree命令
图6.6.5.4 Ctrl-Z之后运行fg命令
图6.6.5.5 Ctrl-Z之后运行kill命令
6.7本章小结
本章简述了进程管理的一些简要信息,比如进程的概念作用,shell的基本原理,shell如何fork和execve我们的hello进程,我们的hello进程在执行时会遇到什么样的情况它是怎么被执行的。又介绍了一些常见异常和其信号处理方法。
7.1 hello的存储器地址空间
图7.1 几个地址的关系
逻辑地址:是指由程序产生的与段相关的偏移地址部分。一个逻辑地址由两部份组成,段标识符和段内偏移量。
线性地址:是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。
虚拟地址:虚拟地址是程序保护模式下,这样程序访问存储器所使用的逻辑地址称为虚拟地址,与实地址模式下的分段地址类似,虚拟地址也可以写为“段:偏移量”的形式,这里的段是指段选择器。
物理地址:在存储器里以字节为单位存储信息,为正确地存放或取得信息,每一个字节单元给以一个唯一的存储器地址,称为物理地址(Physical Address),又叫实际地址或绝对地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段式存储管理要求每个作业的地址空间按照程序自身的逻辑划分为若干段,每个段都有一个唯一的内部段号。
对于intel 的逻辑地址由段标志符和段内偏移量组成。段标志符有16位,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节。
————————————————
| **** 段号S********| ***段内偏移量W|
31——————16 15——————0
———————————————
| *****索引号(13位) | 硬件细节|
15————————3 2————0
索引号就使我们的逻辑地址对应着段描述符表中一个具体的段描述符。
图7.2.1 段描述符
而段描述符又包括全局和局部,一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。当段选择符中的T1字段=0,表示用GDT,=1表示用LDT。
图7.2.2 段式管理
总之对于一个逻辑地址如何转化为线性地址分以下几步:
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
所有的I/ O 设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O:
8.2 简述Unix IO接口及其函数
Unix I/O接口:
1)打开文件:一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。应用程序只需要记住这个描述符。
2)Linux Shell创建的每个进程都有三个打开的文件:标准输入(描述符为0),标准输出(描述符为1)和标准错误(描述符为2)。头文件
3)改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置 k,初始为0。这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek操作,显式地将改变当前文件位置为k。
4)读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到 k+n。给定一个大小为m字节的文件,当k>=m时执行读写操作会触发一个称为end-of-file(EOF)的条件,应用程序能检测到这个条件。类似地,写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
5)关闭文件:当应用完成了对文件的访问之后,它就会通知内核关闭这个文件。作为相应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。无论一个进程因何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
Unix I/O函数:
图8.3.3 write的汇编语句
write前面构建了一些参数,ecx中是要打印出的元素个数 ;ebx中的是要打印的buf字符数组中的第一个元素,最后一句调用了syscall函数。我们再来看看syscall的实现。
sys_call:
call save
push dword [p_proc_ready]
sti
push ecx
push eb
call [sys_call_table + eax * 4]
add esp, 4 * 3
mov [esi + EAXREG - P_STACKBASE], eax
cli
ret
其他涉及太多不提,这个函数的功能就是不断的打印出字符,直到遇到:’\0’ 。
ecx中是要打印出的元素个数 ,ebx中的是要打印的buf字符数组中的第一个元素 。
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
(以下格式自行编排,编辑时删除)
异步异常-键盘中断的处理:键盘中断处理子程序。,第一次getchar()时,确实需要人工的输入,但是如果你输了多个字符,以后的getchar()再执行时就会直接从缓冲区中读取了。实际上是 输入设备->内存缓冲区->程序getchar
getchar等调用read系统函数,通过系统调用读取按键ascii码,,一旦键入回车,getchar就进入缓冲区读取字符,一次只返回第一个字符作为getchar函数的值,如果有循环或足够多的getchar语句,就会依次读出缓冲区内的所有字符直到’\n’
8.5本章小结
本章到了hello实现的最后一步,临门一脚。当我们把前面的内在要求完成之后,最后面对了出结果的时候,我们要实现最后的输出,将hello程序的结果展现在大家面前。
先是介绍了Linux的IO设备管理方法和Unix IO接口及其函数,再分析printf和getchar的实现,最终了解了如何输入输出。
用计算机系统的语言,逐条总结hello所经历的过程。
你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
hello经历的过程
感悟:
一个简单的hello.c,一次漫长的大作业,二者交织之后带给我一份难忘的回忆。
初始hello.c,不过是一个简单的程序,却不知其中又别有洞天,就好像王国维先生的“昨夜西风凋碧树,独上高楼,望尽天涯路”。我们在IDE下的一个简单的Build操作,却对应hello的预处理,编译,汇编,链接,一系列过程,每个过程都精准而优雅。在这次实验中,我们亲自进行了这一步步的过程,分析一个个的中间产物和他们的信息,这一部分也是我认为这个实验最难的一部分。枯燥无聊的一步步查找,分析,这种滋味真的不太是种享受。但是,我还是一点点的做了下来,可真是,“衣带渐宽终不悔,为伊消得人憔悴”。然后就是进程管理,存储管理,IO管理。乍一看,和我们的hello进程实现无太大关联,但是不仅hello对这些必不可缺,其他的进程也根本离不开这些管理。只有实现这些我们才能是hello进程成功的运行。经过这之后又有一种“众里寻他千百度,蓦然回首,那人却在灯火阑珊处”的滋味涌上心头。
一次漫长的大作业,一个并不简单的hello.c,一份难忘的回忆。
列出所有的中间产物的文件名,并予以说明起作用。
文件名称 | 作用 |
---|---|
hello.i | 预处理之后的文件 |
hello.s | 编译之后的汇编文件 |
hello.o | 汇编之后的可重定位目标文件 |
hello | 链接之后的可执行目标文件 |
helloobjdump.txt | hello.o的反汇编文件 |
helloobjdump2.txt | hello的反汇编文件 |
hello.elf | hello.o的ELF头 |
hello2.elf | hello的ELF头 |
helloreadelf.txt | hello.o的ELF所有信息 |
helloreadelf2.txt | hello的ELF所有信息 |
[1] Randal E.Bryant, David R.O’Hallaron.深入理解计算机系统(原书第三版). 北京:机械工业出版社,2016.7.
[2] printf函数实现:https://www.cnblogs.com/pianist/p/3315801.html
[3] Robert Love. Linux内核设计与实现(原书第3版). 北京:机械工业出版社,2011-4-30.
[4] 博韦,西斯特(美). 深入理解LINUX内核(第三版). 北京:中国电力出版社,2007-10-01.
[5] 内存管理:http://www.cnblogs.com/edisonchou/p/5115242.html
[6] sleep函数: https://www.ibm.com/support/knowledgecenter/zh/SSMKHH_10.0.0
/com.ibm.etools.mft.doc/bk52030_.htm