计算机系统
大作业
题 目 程序人生-Hello's P2P
计算机科学与技术学院
2021年5月
摘 要
本文通过一个简单hello程序的运行,分析了在Linux 下x86-64系统的环境下,一个程序的完整运行过程。着重研究了程序的预处理,编译,汇编,链接过程,分析了系统在程序运行时的进程管理,存储管理,IO管理。
关键词:计算机系统,gcc,编译,汇编,链接,进程,异常,信号,虚拟内存,Linux X86
目 录
6.2 简述壳Shell-bash的作用与处理流程 - 1 -
7.2 Intel逻辑地址到线性地址的变换-段式管理 - 1 -
7.3 Hello的线性地址到物理地址的变换-页式管理 - 1 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 1 -
第1章 概述
1.1 Hello简介
1.1.1 P2P
hello.c经过预处理器,汇编,编译,链接,形成可执行目标文件。我们加载并执行可执行目标文件,系统运行hello,形成了进程。hello也从program转变为了process。
1.1.2 O2O
当hello要运行时,内核会在虚拟内存中加载并运行hello的相关代码和数据。当hello结束运行后,父进程便会回收hello,删除相关数据。hello页实现了从Zero-0到Zero-0。
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
Debian GNU/Linux 10 (buster)
Visual Studio Code
gcc 8.3.0
gdb
readelf
edb
1.3 中间结果
文件 | 作用 |
hello.c | hello程序的源代码 |
hello.i | hello程序经过预处理后产生的代码 |
hello.s | hello程序编译产生的汇编代码 |
hello.o | hello程序汇编产生的可重定位目标文件 |
hello | 经过链接后的可执行文件 |
dump_asm | hello.o的反汇编文件 |
dump_link | hello的反汇编文件 |
1.4 本章小结
本章简要介绍了hello程序运行的整个过程,解释了P2P以及O2O的含义,并提供了实验过程中的中间文件。
第2章 预处理
2.1 预处理的概念与作用
C语言预处理器(The C Preprocessor, cpp)会将代码在编译前做相应转换。常见的进行转换的代码部分有:
头文件(Header files):
头文件一般有两种目的:1.系统头文件提供了部分操作系统结构。2.我们自定义的头文件,它包含着我们代码的接口。
宏(Macros):
宏可以理解为代码片段的别名。使用宏时,我们会使用实际的代码代替宏。
条件编译(Conditionals):
条件指令能够指示cpp是否向编译器包含相关的代码段。常见的条件指令有:#if,#elif,#endif等
此外,还有Diagnostics,Line Control,Pragmas等会被cpp处理
2.2在Ubuntu下预处理的命令
使用cpp hello.c > hello.i 生成hello.c预处理后的文件。
2.3 Hello的预处理结果解析
观察生成的hello.i。我们发现源代码中填充了大量其他代码。这些是cpp所填充的。
观察hello.i与stdio.h部分内容。
发现十分相似,也就是说cpp对与头文件包含做了一定的预处理。
2.4 本章小结
本章研究了C预处理的概念与作用。使用cpp对hello.c进行了预处理,并与源文件,头文件进行比对,更直观地观察了cpp的作用。
第3章 编译
3.1 编译的概念与作用
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
在编译阶段,编译器(cc1)能够将文本文件hello.i翻译成文本文件hello.s。hello.s由汇编代码构成。
3.2 在Ubuntu下编译的命令
gcc -S -no-pie -fno-PIC hello.i -o hello.s
3.3 Hello的编译结果解析
3.3.1 伪指令
伪指令用于指导汇编器和链接器的工作。
.file "helloc.c" #声明源文件
.text #指示为代码段
.section .rodata #指示为rodata节
等
3.3.2 rodata节数据
如图,在.rodata段,我们有两个数据。一个是.LC0它存储的是我们第一条printf语句打印的字符串"用法: Hello 学号 姓名 秒数!\n"。另一个是.LC1,它是第二条printf语句所打印的字符串,其中的%s是占位符。
3.3.3 局部变量
hello.c中main函数的局部变量是i。
用于进行遍历操作。
在汇编代码中,局部变量i别放在了栈上。
我们观察到,-4(%bp)被赋予了0的初值,然后与7进行比较,参与了循环。也就是说,我们的局部变量i存储在了-4(%rbp)的位置。
3.3.4 赋值操作
使用数据传送命令,我们可以进行赋值操作。最简单形式的数据传输类型是MOV类,MOV有movb,movw,movl,movq。分别操作1、2、4、8字节的数据。mov操作的源操作数可以是:立即数、寄存器、内存。目的操作数可以是:寄存器、内存。x86-64规定两个操作数不能都指向内存。
在hello程序中,有赋值语句i=0。我们观察它的汇编代码:
也就是说,通过movl,我们将立即数0,放入i中(i是int型,占4个字节,与movl对应)。
3.3.5 算术运算
x86-64中的运算指令如下图所示:(图源于csapp)
hello程序中对于i有自加运算,观察汇编代码有:
也就是说,汇编语言使用addl对i加上了立即数1,完成了i的自加操作。
3.3.6 关系运算
CPU维护着一组单个位的条件码(condition code),它们描述了最近算数运算或逻辑运算的属性。常见的条件码有:
CF:进位标志
ZF:零标志
SF:符号标志
OF:溢出标志
算数运算会设置条件码。此外,CMP指令和TEST指令能够设置条件码并不改变其他寄存器。CMP指令与SUB指令行为一致;TEST指令与AND指令行为一致。
在我们的hello程序中,出现了i<8的关系运算,我们观察它的汇编代码实现:
可以看到,i<8是由cmpl $7, -4(%rbp)实现的。这之后,汇编代码使用了jle来进行跳转。当i小于等于7(也就是i<8)时,程序会正常跳转至.L4。
3.3.7 数组运算
对于数据类型T和整型常数N。若我们有:
T A[N]
若其其实位置为xa,那么数组索引i所指示元素的会被存放在xa+L×i的地址。
在我们的hello程序中,我们访问了argv[1]与argv[2]两个数据。
我们观察相应的汇编代码:
这里-32(%rbp)的地址是argv的地址(数组首地址)。-32(%rbp)+16是argv[2]的地址,它指向的值被放入了%rdx; -32(%rbp)+8的地址是argv[1]的地址,它指向的值被放入了%rsi。argv数组中存储的数据类型是char*,占据8个字节。而-32(%rbp)+16与-32(%rbp)+8恰好差距8个字节,这与我们的知识相符。
3.3.8 控制转移
条件分支的实现由两种主要的方式:一种是通过结合有条件跳转和无条件跳转,这是使用控制的条件跳转;另一种是计算两种一个条件操作的两种结果,根据条件是否满足从中选取一个。
hello程序有一个if(argc!=4)的条件判断,我们观察它的汇编代码:
这是有条件跳转,如果-20(%rbp)的值与4相等,我们便会前往.L2。
C语言有多种循环结构。do-while循环,我们可以翻译为:
loop:
body-stat;
t=test-expr;
if(t)
goto loop;
对于while循环,我们有两种翻译方式,一种是jump to the middle:
goto test;
loop:
body-stat;
test:
t=test-expr;
if (t)
goto loop;
另一种是guarded-do:
t=test-expr;
if (!t)
goto done;
loop:
body-expr;
t=test-expr;
if (t)
goto loop;
done:
对于for循环,GCC会产生while循环中的一种。
hello程序中有关于i的for循环,我们观察相应的汇编代码。
这是典型的jump to the middle。首先在.L2中进行变量初始化,然后跳转到.L3进行条件测试,然后进入中间.L4执行循环体。
3.3.9 函数操作
在函数调用时,包括了许多机制:(P调用Q)
传递控制。程序计数器设置为Q的地址,返回时,计数器设为Q之后指令的地址。
传递数据。P向Q提供一个或多个参数,Q向P提供一个返回值。
分配、释放内存。
hello程序中,我们调用了printf, sleep等函数。我们观察printf("用法: Hello 学号姓名秒数!\n");的汇编代码:
可以观察到,我们将字符串.LC0放入了寄存器%edi作为参数,通过call调用了函数puts。
x86-64中,我们最多可以通过栈传递6个整型参数。参数有特定顺序如下:(图源于csapp)
也就是说,位于.LC0的字符串是puts函数的第一个参数。
3.4 本章小结
本章简述了编译的概念与作用,对hello.s中的汇编代码进行了简单解析,梳理了C中局部变量、赋值操作、算术运算、数组运算在汇编语言中的解释。
第4章 汇编
4.1 汇编的概念与作用
汇编器(as)将hello.s翻译为机器语言,产生可重定位目标程序,生成hello.o文件。hello.o文件是二进制文件。需要注意的是,此时的hello.o还未进行链接,所以不可直接运行。
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
4.2 在Ubuntu下汇编的命令
gcc -c -no-pie -fno-PIC hello.s -o hello.o
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
readelf -u hello.o直接查看全部信息。
首先是elf文件的文件头信息:
我们可以获取elf文件的类型-可重定位文件,程序系统64位等信息。
程序各个节的信息:
我们观察到有.text节、.data节、.bss节以及他们的偏移量等信息。
重定位信息:
可以观察到,.rela.text是重定位节,其中有puts, exit等符号需要进行重定位。.rodata+0符号的重定位类型是R_X86_64_32,puts等符号的重定位类型是R_X86_64_PLT32。
符号表信息:
符号表中记录了我们程序中的符号以及他们的类型、位置等信息。
4.4 Hello.o的结果解析
objdump -d -r hello.o
反汇编代码于汇编代码的比较:(左为汇编代码,右为反汇编代码)
可以观察到,二者十分相似。但是许多地方不相同。
反汇编代码中不再有汇编代码中的伪节。
在分支转移中,二者也不相同:(左为汇编代码,右为反汇编代码)
可以观察到,汇编代码需要借助于伪指令,而反汇编代码中直接跳转到了相应地址。
重定位信息的处理,二者也不同:(左为汇编代码,右为反汇编代码)
可以观察到,汇编代码直接访问了.rodata节的数据,直接按函数名调用了函数,而反汇编代码中二者均未填入真实地址,需要进行重定位。
4.5 本章小结
本章简述了汇编的概念与作用,对hello.s进行汇编生成了相应的可重定位目标程序hello.o。使用readelf工具读取了elf文件相关信息,观察了elf文件中的文件头、程序节、符号表等信息。比较了hello.s与hello.o反汇编形成的汇编代码的异同。
第5章 链接
5.1 链接的概念与作用
通过链接器(linker)ld,我们能够将需要的目标文件链接到已经生成好的hello.o目标文件中,形成可执行目标文件。可执行目标文件能够被加载到内存中,由系统执行。
链接可以执行于编译时(complie time)、加载时(load time)、运行时(run time)。链接使得分离编译(separate compilation)成为可能。我们可以将大型程序分解为更下、更好管理的模块,可以独立地修改和编译这些模块。
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.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
文件头信息:
可以观察到,hello文件的类型是EXEC,也就是我们常说的可执行目标文件。
节信息:(部分)
用于共享库的符号:
符号表:(部分)
可以观察到,符号表中有许多共享库的符号。
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
可以看到程序起始虚拟地址是0x400000
5.3部分readelf得到程序的入门.init节的地址是0x401000,edb与之相对应:
5.3部分readelf得到程序的代码段.text的地址是0x401090,观察edb:
观察可得,程序代码段地址确为0x401090。
5.5 链接的重定位过程分析
首先,我们观察hello.o的反汇编代码。可以观察到,有许多地方并没有填入正确的地址,正等待进行链接。其中R_X86_64_32表示的类型是32位直接寻址,相应地方会填入.rodata的真实地址。而R_X86_64_PLT32表示puts函数和exit函数需要通过共享库进行动态链接。
在hello文件的反汇编代码中,我们发现之前的重定位地址已经被填入了正常的地址。观察elf文件信息,我们发现402008恰好属于.rodata节,而401030和401070恰好属于.plt节。
5.6 hello的执行流程
0x00007ffff7e16e20 <_init>
0x0000000000401090<_start>
0x0000000000401150<in __libc_csu_init>
0x0000000000401000<_init>
0x00000000004010c5<main>
0x0000000000401030<puts>
0x0000000000401070<exit>
5.7 Hello的动态链接分析
首先查看与动态链接相关的.plt段和.got段。
我们观察链接前的.got.plt表中内容:
执行链接后,有:
可以观察到,0404000到0404010之间的一段数据发生了变化。此变化便是GOT表中加载了共享库的内容。
5.8 本章小结
本章简述了链接的概念与作用,分析了我们经过链接生成的hello 文件的结构以及与之前经过链接的hello.o文件的异同,我们分析了hello文件的运行流程,使用edb探索了动态链接的过程。
第6章 hello进程管理
6.1 进程的概念与作用
进程经典定义是执行中程序的一个实例。系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。
进程能够提供给应用程序一些关键抽象:
-
一个独立的逻辑控制流。进程使得我们感觉好像在独占地使用处理器。
-
一个私有地址空间。进程使得我们感觉好像独占地使用内存系统。
6.2 简述壳Shell-bash的作用与处理流程
shell是命令行界面的解析器,能够为用户提供操作界面,提供内核服务。shell能执行一系列的读、求操作,然后终止。读操作读取来自用户的一个命令行。求值操作解析命令并代表用户运行程序。
shell的处理流程为:
读取用户输入
解析用户输入
若要执行内部命令,直接执行
若要执行非内部命令,shell会fork子进程,在子进程中execve执行相关命令
根据&的有无,确定程序的前后台运行
6.3 Hello的fork进程创建过程
当我们在命令行输入.hello以执行我们的hello程序时,由于hello程序并不是shell程序的内部程序,所以shell会使用fork来创建子进程并进行后续操作。
新创建的子进程几乎和父进程相同。子进程拥有于父进程用户级虚拟地址空间相同且独立的一份副本,与父进程任何打开文件描述符相同的副本。
我们使用fork函数来创建一个子进程,fork函数的函数原型为:pid_t fork(void)
对于fork函数我们需要注意:
-
调用一次,返回两次。一次返回至父进程,返回的是子进程的pid;一次返回至子进程,返回值为0.
-
并发执行。父子进程是并发运行的独立进程。
-
相同但独立的地址空间。子进程创建时,两个进程具有相同的用户栈、本地变量、堆、全局变量、代码。但是二者对这些的改变都是相互独立的。
-
共享文件。
6.4 Hello的execve过程
使用fork创建子进程后,子进程便会使用execve加载并运行hello程序,且带参数列表argv以及环境变量envp。execve调用一次,从不返回。
可以观察到,argv指向一个指针数组,这个指针数组中的每一个指针指向一个参数字符串。其中argv[0]是我们所运行程序的名字。envp指向一个指针数组,这个数组里面的每一个指针指向一个环境变量字符串。环境变量字符串的格式为"name=value"。我们可以使用getenv函数获取环境变量,setenv、unsetenv来设置、删除环境变量。
execve会调用调用启动加载器。加载器会删除子进程现有的虚拟内存段,创建一组新的代码、数据、堆、栈。新的栈和堆被初始化为0。通过虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码数据初始化。最后,跳转到_start地址,最终调用main函数。
6.5 Hello的进程执行
系统中每个程序都运行在某个进程的上下文中。上下文是程序正确运行所需要的状态,由内核进行维持。
一个运行多个进程的系统,进程逻辑流的执行可能是交错的。每个进程执行它的流的一部分,然后被抢占,然后轮到其他进程。一个逻辑流在时间上与另一个重叠,成为并发流。一个进程执行它的控制流的一部分时间叫做时间片。
控制寄存器利用模式位描述了当前进程享有的特权:当设置了模式位时,进程运行在内核模式中,可以执行任何命令,访问任何内存;当没有设置模式位时,进程为用户模式,不允许执行特权指令,不允许直接引用内核区的代码、数据。
在进程执行时,内核可以抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策称为调度。当进程调度一个新的进程运行后,会使用上下文切换来将控制转移到新的进程。上下文切换会:1.保存当前进程的上下文。2.恢复某个先前被抢占进程的被保存的上下文。3.将控制传递给新进程。系统调用、中断可能引起上下文切换。
下图反映了进程上下文切换的过程:(源于csapp)
我们的hello程序执行了sleep系统调用,引发了陷阱异常。此时会从用户模式进入内核模式,使程序休眠一段时间,将控制转给其他进程。当sleep结束后,发送信号给内核,进入内核状态处理异常,此时hello程序得以重新回到用户模式。当执行getchar函数时,会使用read系统调用,产生上下文切换。
6.6 hello的异常与信号处理
6.6.1 异常
异常可以分为四类:中断、陷阱、故障、 终止。
hello运行过程中,可能出现以上集中异常:
类别 | 可能原因 |
中断 | hello运行过程中遇到来自IO设备的中断,例如键盘 |
陷阱 | hello运行过程中调用了sleep系统调用 |
故障 | hello运行时,发生缺页异常 |
终止 | hello运行时发生硬件错误,如DRAM、SRAM的奇偶错误 |
hello 运行过程中的异常与信号处理
正常运行:
使用Ctrl+C发出SIGINT信号,使得程序终止:
使用Ctrl+Z发出SIGTSTP信号,程序停止直至下一个SIGCONT:
使用ps查看进程信息:
使用jobs查看作业信息,可以发现hello程序处于停止状态:
pstree查看进程树:
使用fg将后台程序置于前台:
使用kill发送信号:
6.7本章小结
本章简述了进程、shell的概念与作用,分析了hello程序使用fork创建子进程的过程以及使用execve加载并运行用户程序的过程,运用上下文切换、用户模式、内核模式、内核调度等知识,分析了hello进程的执行过程,最后分析了hello对于异常以及信号的处理并进行了实际操作。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:编译器进行编译时,产生汇编汇编代码,每局代码以及每条数据都会有自己的逻辑地址。
线性地址:CPU加载程序后,会为程序分配内存,分配的内存分为代码段数据和数据段数据。代码段基地址在CS中,数据段基地址在DS中。线性地址=段基址+逻辑地址
虚拟地址:虚拟内存中的地址
物理地址:物理内存中的地址
7.2 Intel逻辑地址到线性地址的变换-段式管理
段是对程序逻辑意义上的一种划分,一组完整逻辑意义的程序被划分为一段。段的长度不确定。段描述符用于描述一个段的详细信息。段选择符用于找到对应的段描述符。流程:
通过段选择符的T1字段,确定是GDT中段还是LDT中的段。
查找段描述符,获得基地址
基地址+偏移,得到线性地址
7.3 Hello的线性地址到物理地址的变换-页式管理
虚拟地址包含两个部分:VPN(虚拟页号)、VPO(虚拟页面偏移)
页表基址寄存器指向当前的页表。MMU通过VPN在页表中找到对应的页表项PTE。将PTE中的PPN与VPO串联,得到最终的物理地址。流程可见:
进一步,我们可以将告诉缓存与虚拟内存相结合,告诉缓存可以存储我们的PTE以及数据,如下图:
进一步,我们可以使用TLB来加速虚拟内存。TLB是翻译后备缓冲器,是一个小的、虚拟寻址的缓存,每一行保存单个PTE组成的块。
7.4 TLB与四级页表支持下的VA到PA的变换
我们具体分析运行Linux的Intel Core i7,它使用了TLB以及四级页表。Core i7支持48位的虚拟地址空间以及52位的物理地址空间。
首先,我们分析各级页表中条目的格式。
第一、二、三级页表条目格式如下:
他们存储的物理页号为40位。
第四级页表条目格式:
它存储的物理页号也为40位。
PTE中有三个权限位:
R/W 确定读写还是只读
U/S 确定用户模式时候可访问
XD 禁止执行
其他位:
A 引用位,用于实现替换算法
D 修改位,告知是否写回
进行翻译时,虚拟地址中的VPN被划分为VPN1,VPN2,VPN3,VPN4。CR3寄存器中有L1页表的地址,根据VPN1能够在L1页表找到相应PTE,得到L2页表的基地址,一次类推,最终我们得到物理地址。并进行之后访问。具体流程如下:
(以下格式自行编排,编辑时删除)
7.5 三级Cache支持下的物理内存访问
得到物理地址PA后,我们将物理地址进行分割。物理地址(52位)被分割为40位的标记位CT,6位的索引位CI,6位的块偏移CO。通过CT查找告诉缓存中的对应块,通过CI在块中寻找行,若命中,则返回对应块偏移的数据。否则,L1不命中,我们需要前往L2,L3甚至是主存中得到对应的数据。
具体流程如下:
7.6 hello进程fork时的内存映射
执行fork函数时,内核会为新进程创建各种数据结构,分配pid。创建新进程的虚拟内存,新进程会拥有当前进程mm_struct,区域结构,页表的副本。但是两进程页面只读,区域写时复制。
7.7 hello进程execve时的内存映射
删除已存在的用户区域
映射私有区域:所有这些新的区域都是私有的、写时复制的。代码和数据被映射至.text区和.data区。bss区域,堆是请求二进制零的区域。
映射共享区域:hello程序与共享对象链接,这些对象动态链接到hello程序,然后映射到用户虚拟地址空间。
设置程序计数器:设置当前进程上下文的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
当MMU翻译某个地址时,触发了缺页异常。此时,控制便会转移到内核的缺页处理程序。处理程序会判断:
-
虚拟地址是否合法。判断虚拟地址是否处在区域结构定义的区域内。若不合法,便会触发段错误。
-
试图进行内存访问是否合法。判断进程是否有足够的权限。
当内核知道缺页是正常情况时,会选择牺牲页,换入新的页面并进行页表更新,完成缺页处理。
7.9动态存储分配管理
动态内存分配器维护着进程的虚拟内存区域,成为堆。分配器将堆视为一组不同大小块的集合。各个块是已分配的或者是空闲的。分配器有两种基本风格:显式分配器,隐式分配器。
C语言中,可以使用malloc和free函数来动态申请、释放内存。
7.9.1 隐式空闲链表
空闲块可以通过头部中的大小字段隐含地连接着。我们可以通过遍历堆中所有的块来间接遍历整个空闲块集合。
隐式空闲链表块的格式:
隐式空闲链表的整体形式:(注意到序言块和结尾块的存在)
7.9.2 带边界标记的合并
通过双界标记,我们可以在常数时间内完成空闲块的合并,具体结构如下:
7.9.3 显式空闲链表
将空闲链表组织成一种显式的数据结构。在空闲块主体中放入指针。这样,我们可以快速定位到前、后的空闲链表,使首次适配的时间减少为空闲块的个数的线性时间,具体格式如下:
7.9.4 分离的空闲链表
维护多个空闲链表,将所有可能的块大小划分为大小类。基本方法有:1.简单分离存储。2.分离适配。3.伙伴系统。
7.10本章小结
本章简述了系统对于hello的存储管理,介绍了intel段式、页式管理,分析了程序的虚拟地址逐步翻译为物理地址的过程,分析程序运行过程中fork,execve函数进行的内存映射,说明了系统对于缺页异常的处理以及动态啊存储的分配。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
一个Linux文件就是一个m字节的序列。所有的IO设备都被模型化为文件,所有的输入输出都被当做对相应文件的读和写来执行。这允许Linux内核引出一个简单、低级的应用结构,使得所有的输入输出能够以一种统一且一致的方式来执行。
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
8.2.1 接口
打开文件:通过内核代开文件,内核返回非负整数,成为描述符。描述符表示这个文件。内核记录有关文件的所有信息。
文件位置。每个打开的文件,内核保持一个文件位置k,表示从文件开头起始的字节偏移量。
读写文件。进行复制操作并改变文件位置k的值。
关闭文件。内核释放相应数据结构,将描述符恢复到可用的描述符池中。
8.2.2 函数
int open(char *filename, int flags, mode_t mode)
将filename转换为文件描述符,返回描述符数字,总返回进程中没有打开的最小描述符。
int close(fd)
关闭一个打开的文件。
ssize_t read(int fd, void *buf, size_t n);
从fd复制至多n个字节到buf
ssize_t write(int fd, const void *buf, size_t n);
从buf复制至多n个字节到fd
8.3 printf的实现分析
观察printf的源码:
int printf(const char *fmt, ...)
{
int i;
char buf[256];
va_list arg = (va_list)((char*)(&fmt) + 4);
i = vsprintf(buf, fmt, arg);
write(buf, i);
return i;
}
其中vsprintf(buf, fmt, arg)函数能够返回我们想要打印的字符串的长度并对我们的格式化字符串进行解析。当获取到字符串的长度后,我们便能够将字符串打印出来。
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
getchar能够读取stdin,然后获取输入的字符。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章简述了Linux的IO设备管理方法以及UNIX IO接口及其函数,简单解释了printf以及getchar函数实现的基本原理。
结论
用计算机系统的语言,逐条总结hello所经历的过程。
-
初始时,hello.c是存储在磁盘上的一个文本文件。
-
hello.c经过cpp进行了预处理,形成了另一个文本文件hello.i
-
hello.i经过编译器cc1翻译成文本文件hello.s,hello.s中包含着汇编语言代码。
-
hello.s经过汇编器as翻译成机器语言指令,并打包形成可重定位目标文件。
-
链接器ld将hello.o需要的各种目标文件与hello.o进行链接,形成可执行目标文件hello
-
使用shell执行hello
-
shell使用fork创建子进程,使用execve加载并运行hello程序。这一过程中,涉及到了虚拟内存,内存映射等知识。
-
hello运行过程中,可能要到各种异常,收到各种信号,hello可能需要陷入到内核,调用异常处理程序。
-
hello调用printf,使用UNIX IO来进行输出
-
hello运行结束,被父进程回收
抽象是计算机科学中的重要概念。计算机系统也体现了这一理念,例如文件是对IO设备的抽象,虚拟内存是对程序存储器的抽象等。
我们还需注意计算机系统在整体上的统一与系统。一个功能可能涉及和影响到许多其他功能。系统是硬件和软件相互交织的集合体,它们必须共同协作才能达到程序的最终目的。
学习计算机系统,需要不断地实践,不断地实验,从实实在在的真实系统中,收获经验以及各种设计思想。
附件
文件 | 作用 |
hello.c | hello程序的源代码 |
hello.i | hello程序经过预处理后产生的代码 |
hello.s | hello程序编译产生的汇编代码 |
hello.o | hello程序汇编产生的可重定位目标文件 |
hello | 经过链接后的可执行文件 |
dump_asm | hello.o的反汇编文件 |
dump_link | hello的反汇编文件 |
MakeFile | 包含了实验过程中各个中间指令 |
ICS大作业论文.docx | |
ICS大作业论文.pdf |
参考文献
[1] https://gcc.gnu.org/onlinedocs/cpp/index.html#Top
[2] https://www.calleluks.com/the-four-stages-of-compiling-a-c-program/
[3] https://zhuanlan.zhihu.com/p/90004914