计算机系统
大作业
计算机科学与技术学院
2019年12月
摘 要
本文叙述程序源代码通过预处理、编译、汇编、链接等步骤成为可执行程序、并通过进程管理、存储管理、IO管理等系统机制在操作系统中得以运行和结束的整个过程,解释了程序的“P2P”(From Program to Process)和“O2O”(From Zero-0 to Zero-0),以此回顾并串联了计算机系统各章节的知识。
关键词:预处理;编译;汇编;链接;进程管理;存储管理;IO管理
目 录
第1章 概述 - 5 -
1.1 HELLO简介 - 5 -
1.2 环境与工具 - 5 -
1.3 中间结果 - 5 -
1.4 本章小结 - 6 -
第2章 预处理 - 7 -
2.1 预处理的概念与作用 - 7 -
2.2在UBUNTU下预处理的命令 - 7 -
2.3 HELLO的预处理结果解析 - 7 -
2.4 本章小结 - 8 -
第3章 编译 - 10 -
3.1 编译的概念与作用 - 10 -
3.2 在UBUNTU下编译的命令 - 10 -
3.3 HELLO的编译结果解析 - 10 -
3.3.1 常量 - 10 -
3.3.2 变量 - 11 -
3.3.3 赋值 - 11 -
3.3.4 运算 - 11 -
3.3.5 关系操作 - 12 -
3.3.6数组操作 - 12 -
3.3.7 控制转移 - 12 -
3.3.8 函数操作 - 13 -
3.4 本章小结 - 13 -
第4章 汇编 - 14 -
4.1 汇编的概念与作用 - 14 -
4.2 在UBUNTU下汇编的命令 - 14 -
4.3 可重定位目标ELF格式 - 14 -
4.3.1 elf头 - 15 -
4.3.2 节头部表 - 15 -
4.3.3 重定位条目 - 16 -
4.4.4 符号表 - 17 -
4.4 HELLO.O目标的结果解析 - 17 -
4.5 本章小结 - 18 -
第5章 链接 - 19 -
5.1 链接的概念与作用 - 19 -
5.2 在UBUNTU下链接的命令 - 19 -
5.3 可执行目标文件HELLO的格式 - 19 -
5.4 HELLO的虚拟地址空间 - 22 -
5.5 链接的重定位过程分析 - 24 -
5.6 HELLO的执行流程 - 24 -
5.7 HELLO的动态链接分析 - 25 -
5.8 本章小结 - 25 -
第6章 HELLO进程管理 - 26 -
6.1 进程的概念与作用 - 26 -
6.2 简述壳SHELL-BASH的作用与处理流程 - 26 -
6.3 HELLO的FORK进程创建过程 - 26 -
6.4 HELLO的EXECVE过程 - 26 -
6.5 HELLO的进程执行 - 26 -
6.6 HELLO的异常与信号处理 - 27 -
6.6.1 回车 - 27 -
6.6.2 Ctrl-Z - 27 -
6.6.3 Ctrl-C - 28 -
6.6.4不停乱按 - 28 -
6.7本章小结 - 28 -
第7章 HELLO的存储管理 - 29 -
7.1 HELLO的存储器地址空间 - 29 -
7.2 INTEL逻辑地址到线性地址的变换-段式管理 - 29 -
7.3 HELLO的线性地址到物理地址的变换-页式管理 - 29 -
7.4 TLB与四级页表支持下的VA到PA的变换 - 30 -
7.5 三级CACHE支持下的物理内存访问 - 32 -
7.6 HELLO进程FORK时的内存映射 - 32 -
7.7 HELLO进程EXECVE时的内存映射 - 33 -
7.8 缺页故障与缺页中断处理 - 34 -
7.9动态存储分配管理 - 34 -
7.10本章小结 - 35 -
第8章 HELLO的IO管理 - 36 -
8.1 LINUX的IO设备管理方法 - 36 -
8.2 简述UNIX IO接口及其函数 - 36 -
8.3 PRINTF的实现分析 - 37 -
8.4 GETCHAR的实现分析 - 37 -
8.5本章小结 - 38 -
结论 - 38 -
附件 - 39 -
参考文献 - 40 -
第1章 概述
1.1 Hello简介
P2P:
高级语言编写的程序源代码.c文件,经过预处理得到.i文件,由编译转换成汇编语言编写的.s文件,再由汇编器生成可重定位目标文件.o,和库文件链接,生成可执行程序。在shell中运行这个可执行程序,会分配进程空间,将可执行程序装入使其运行。从程序到进程,实现了P2P(From Program to Process)的过程。
O2O:
Shell中原本不存在这个程序,输入命令运行程序时,shell调用fork函数创建一个新的子进程,再调用execve使内核为它开辟空的空间和数据结构,将程序内容装入其中,这是从无到有的过程;当程序运行结束后,内核将清空它留下的数据和空间,逐步清除程序运行的痕迹,最终回到程序运行前的状态,这是从有到无的过程。从无到有再到无,实现了O2O(From Zero-0 to Zero-0)的过程。
1.2 环境与工具
硬件环境:Intel i5-4210H (4) @ 3.500GHz 8GBRAM
软件环境:Windows 10 64位,Arch Linux x86_64
开发工具:gcc,objdump,gdb ,readelf,sublime text
1.3 中间结果
- hello.i 预处理后的源代码文件
- hello.s hello.i编译后的汇编文件
- hello.o hello.s汇编后的可重定位目标文件
- hello hello.s与其他可重定位目标文件链接后的可执行程序
- hello.txt hello.o的反汇编代码
- hello1.txt hello的反汇编代码
- hello.elf hello.o的elf文件
- hello1.elf hello的elf文件
1.4 本章小结
本章简单描述了hello的P2P与O2O过程,说明了实验的准备工作和生成产物。
第2章 预处理
2.1 预处理的概念与作用
在计算机编程中,预处理是在进行下一步翻译之前对源代码执行处理的阶段。在c语言中,预处理是编译的第一个阶段,主要是根据代码中#开头的指令对代码进行分割、替换等处理,这些指令被称为预处理指令。预处理的结果是生成.i文件。
预处理的作用主要有以下几种。一是文件包含,即只要一条指令就相当于另一个文件中的很多内容,方便在不同源文件中调用相同的内容;二是宏定义,即define指令,把代码中的某一段文本替换成另一段文本,有减少大量重复代码、避免出现常数等作用;三是条件编译,即根据环境条件判断某段代码该不该编译。除此之外,还有删除注释、添加行号和标记、添加控制信息等作用。经过预处理的代码变得简单直接,适合进一步编译成机器语言。
2.2在Ubuntu下预处理的命令
gcc -m64 -no-pie -fno-PIC -E hello.c -o hello.i
2.3 Hello的预处理结果解析
可以看出,程序的主要部分被保留成最简单的格式,删去了所有注释,位于.i文件的第3023行;而前面包含头文件的预处理指令则变成了大段头文件本身的内容,并递归地添加头文件中又包含的文件,直到编译所需的所有代码都被包含在内。文件添加了行号便于编译时分析。
2.4 本章小结
本章解释了预处理的概念和作用,并通过hello.i这一例子展示了预处理如何对待程序源代码,体现出预处理后的文件更加简单、直接、全面,有利于进一步编译,说明了预处理的好处和重要性。
第3章 编译
3.1 编译的概念与作用
C语言程序编译过程内的“编译”步骤,是指将.i形成的简单直接的c语言码经过分析并优化,翻译成一份汇编语言代码,得到的结果为.s文件。
编译的作用是把c语言翻译成汇编语言并进行优化,使其更加接近机器二进制语言,得到更简单明了和更符合机器运作规律的逻辑,方便下一步的汇编步骤。
3.2 在Ubuntu下编译的命令
gcc -m64 -no-pie -fno-PIC -S hello.c -o hello.i
3.3 Hello的编译结果解析
3.3.1 常量
.section .rodata说明接下来的是只读数据rodata段。本程序的.rodata段中存储了两个字符串常量,即.LC0和.LC1,分别对应源程序的两个printf。可以看到和源程序相比缺失了一个\n,这是编译器优化成用puts函数输出的结果。.rodata还可能存放const全局变量、switch跳转表等。
程序中间出现的数字常量都以前面加$的立即数的形式写在了汇编代码中。
3.3.2 变量
Hello.c中不存在全局变量,上图为一个局部变量的例子。可以看到定义局部变量时是直接在栈中开辟一块空间,值也是直接被存在栈中的,%rbp就是一个典型的用于作为栈的寄存器。
3.3.3 赋值
还以局部变量i的赋值为例,直接向栈的寄存器中存入32位整型(对应movl的l后缀),即int i = 0。
3.3.4 运算
Hello.c中只有一个运算,即循环中的++运算。实现++运算所用的汇编代码是自增函数addl,直接访问栈来对i进行+1运算。
3.3.5 关系操作
Hello.c中有两个关系运算,分别是!=和<。
在汇编中,!=比较操作采用了简单的cmp函数实现。
这里可以注意到,<8被编译器优化成了<=7。这样优化后就也可以简化为用cmp实现了。
3.3.6数组操作
汇编语言中并没有数组的概念,而是将数组简化为其本质:一串相连的存储空间。要想访问数组中的某一个元素,就用数组的基地址加上偏移量来得到元素地址。
这里是访问数组的三条语句。第一条是获得数组argv的基地址,也就是argv[0],第二条在基地址上加了偏移量,第三条则访问了加偏移量后的地址,也就是argv[1],这里存储着输入的第一个字符串。
3.3.7 控制转移
Hello.c中有两处控制转移,分别是if和for。
上图是if的条件转移,判断相等。J后面加不同的字母是汇编中判断是否相等的语句,其中je是判断相等,如果相等则跳转到.L2处的代码,即if中的代码;否则接着执行下面的代码。
上图是for循环的条件转移,jle是判断小于等于,如果小于等于7则跳到.L4,而.L4在这行上方的循环体开始处,从而开启又一轮循环。
3.3.8 函数操作
函数在汇编中通过寄存器实现,其中返回值存储在%eax中,参数则存储在rdi, rsi, rdx, rcx, r8, r9这六个寄存器中进行调用,如果需要更多参数还要利用栈帧存入栈中,但hello.c中没有用到。调用函数时用到的汇编语句是call,返回时则用ret。以下举例说明几处函数调用。
输出第一个字符串,编译器将原本的printf函数优化成了puts函数,先传入参数即.LC0的字符串,再调用puts。
这段代表exit(1)。
还有atoi、getchar、printf、sleep等函数,传入参数和调用的步骤都大致相同。
以上的函数都是c语言的库函数,没有出现ret。这个ret出现在if的条件结构中,是用于结束if的。
3.4 本章小结
本章解释了c程序编译过程中“编译”步骤的概念和作用,并通过hello.s这一例子展示了编译得到的汇编语言代码,从中可以看到从c代码到汇编代码的翻译方式,也体现出并不是直接死板地翻译,而是要经过修整以适合机器的运作规律,从而离下一步的翻译成二进制机器语言又更进一步。
第4章 汇编
4.1 汇编的概念与作用
汇编是将汇编语言代码翻译成二进制机器语言,并生成可重定位目标文件的过程。
得到的二进制机器语言是机器可以直接理解并运行的,只要再经过链接就可以得到能够运行的完整程序了。
4.2 在Ubuntu下汇编的命令
gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o
4.3 可重定位目标elf格式
以上是elf的基本格式。下面逐一解析这些节分别的作用和意义。
4.3.1 elf头
Elf头开始是一个16字节序列,前四个字节是elf格式固定的开头,然后的三个字节依次代表64位、小端序和文件头版本。Elf头含有文件的最基本信息,是在链接时读取并理解这个文件所必不可少的。
4.3.2 节头部表
节头部表列出了各节的大小、类型、地址、偏移量等信息,方便查找各节。
4.3.3 重定位条目
文件中有一些内存地址或引用,这些地方在链接前是待定的,需要视链接的情况指定确切的地址。因此,需要对这些地址进行重定位。每个代码段或数据段都对应一个重定位表,记录了段中的这些位置,方便对它们进行查找和操作。
4.4.4 符号表
.symtab是符号表,它列举了程序中用到的函数和全局变量。
4.4 Hello.o目标的结果解析
反汇编得到的代码和之前的汇编代码主要有三方面差别:一是操作数,反汇编代码中的操作数从十进制变为了十六进制;二是分支转移,.L2、.L3等段名称在反汇编代码中变成了以偏移量表示的内存地址;三是函数调用,也从函数名变成了以偏移量表示的内存地址。这样的变化,主要是由于机器码中没有符号的概念,所有的符号都要变成具体可查的数字。但内存地址又是偏移量而不是具体的数值,这是因为还没有链接,无法确定使用的内存地址,这部分要留给重定位来解决。
4.5 本章小结
本章解释了c程序编译过程中“汇编”步骤的概念和作用,并以hello.o的elf格式和反汇编代码为例展示了二进制机器语言文件的格式和特性。从汇编代码到二进制机器语言的过程是有迹可循、有理可依的,它为下一步的链接又提供了更方便的条件。
第5章 链接
5.1 链接的概念与作用
链接是结合多个不同的可重定位目标文件、得到具有统一内存地址,能够运行的可执行程序的过程。
链接将不同文件中的数据和程序段结合统一起来,在编程时方便由各个小文件组成大型程序,条理清晰,使得更加分散化、模块化的编程成为可能。
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
ld -o hello -dynamic-linker /lib64/ld-linux-x86-64.so.2 /lib/crt1.o /lib/crti.o hello.o /lib/libc.so /lib/crtn.o
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
Elf头:
节头部表:
程序头表:
符号表:
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
运行main前的memory regions:
运行main后的memory regions:
对比节头部表,得到节映射到的段信息。其中虚拟地址减偏移量的值是0x400000则为代码段,0x600000则为数据段。
5.5 链接的重定位过程分析
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
这是两者反汇编的对比,主要有以下几项区别:以0开头的虚拟地址变成了具体的内存地址;函数的调用也变成了内存地址;增加了.init和.plt节;增加了getchar等库函数。由此可以知道,链接的过程就是将不同.o文件的内容按合理顺序拼接在一起使得彼此能够配合的过程。在重定位时,链接器需要整理符号表中的条目,分配出内存地址。先将每个同类节合并成同一个节,然后为它们赋予内存地址,使指令和变量有唯一的内存地址。最后将重定位节中的符号引用改为内存地址。
5.6 hello的执行流程
ld-2.27.so!_dl_start
ld-2.27.so!_dl_setup_hash
ld-2.27.so!_dl_sysdep_start
ld-2.27.so!_dl_init
libc-2.27.so!_cxa_atexit
libc-2.27.so!_new_nextfn
hello!_init
hello!main
hello!printf@plt
hello!atoi@plt
hello!sleepp@plt
hello!getchar@plt
5.7 Hello的动态链接分析
GOT表,即全局偏移表,对应了所有外部定义的符号,这些符号是需要在程序运行中才能重定位的,称为动态链接。通过.plt表查找数据,再跳转到got表,got表的值会改变,从0变为真实地址。
5.8 本章小结
本章解释了c程序编译过程中“链接”步骤的概念和作用,并以可执行程序的elf格式和反汇编代码为例展示了链接前后的区别,推导出链接的过程。
第6章 hello进程管理
6.1 进程的概念与作用
进程是一个执行中程序的实例,它的上下文提供了程序运行所需的状态。
进程为程序提供逻辑控制流和私有地址空间。
6.2 简述壳Shell-bash的作用与处理流程
Shell是用户与操作系统内核之间的交互程序。
Shell分析用户输入的字符串,先检查是否为内部命令,如果是则执行,不是再查找是否有程序文件,如果有则建立进程和上下文,加载这个程序文件,让系统内核运行这个程序。程序运行后等待其退出并回收进程。
6.3 Hello的fork进程创建过程
父进程调用fork函数,创建pid不同但虚拟地址空间相同的一个副本子进程。子进程和父进程的代码、数据段、堆、共享库以及用户栈均相同,可以访问父进程的文件。Fork函数调用一次返回两次,在父进程中返回子进程的pid,在子进程中返回0.
6.4 Hello的execve过程
Exceve调用加载器,把子进程存在的区域清空,映射新程序自身的私有区域,将代码、数据、共享库和用户栈都加载到上下文中,映射共享区域。最后设置程序计数器指向_start,进而调用main函数。
6.5 Hello的进程执行
一个进程运行时有两种模式,分别是由应用程序控制的用户模式和由内核控制的内核模式。正常情况下是用户模式,拥有仅供正常运行的权限,而故障、暂停、休眠等异常状态时交给系统内核控制,也因此有了更多系统级的权限。在同时运行多个并发进程时,系统将会进行调度,轮流把处理器分配给每个进程以用户模式使用,其他进程就会暂时中断,进入内核模式。进程以用户模式占用处理器的一段时间就成为时间片,多个时间片接续起来成为控制流。进程的上下文中存储了它从中断状态中恢复运行所需的一切信息,因此每个时间片能连接上,就像从未中断过一样顺利完成工作。中断的时间是由sleep函数控制的,当sleep的计时到达预设的时间时就发送信号,让内核重新唤醒这个进程。
6.6 hello的异常与信号处理
6.6.1 回车
正常结束,hello进程已经不再运行了。
6.6.2 Ctrl-Z
按下ctrl-z之后内核将发送一个SIGSTP信号给shell父进程。信号处理函数将hello进程挂起但并不结束,因此ps命令还能看到hello。使用fg命令可以把被挂起的hello重新调度到前台运行,继续将剩下的步骤运行完。
6.6.3 Ctrl-C
按下ctrl-c之后内核将发送一个SIGINT信号给shell父进程。信号处理函数将hello进程挂起并结束,因此ps命令不显示hello,说明hello已经不再运行了。
6.6.4不停乱按
Shell随时用一个getchar函数监听输入的字符,并读取分析回车键后输入的字串,如果是符合语法的命令则在程序结束后将会执行,如果并不是设定好的命令则不会做出任何反应,程序继续运行。
6.7本章小结
本章解释了c程序运行时进程从创建到回收的全过程,说明了多并发进程被内核调度切换的方式、shell和内核如何调度进程并对信号和命令做出反应,并试验了程序运行时几种信号的结果。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:相对于当前程序进程段的地址偏移量,也是经过汇编后程序中写的地址,表示操作数或指令的地址与段基地址之间的距离。
线性地址:32位无符号整数,用段的基地址加上逻辑地址也就是偏移量,就得到了线性地址。
虚拟地址:每个进程拥有4G虚拟内存,高2G属于内核,低2G属于进程独占。虚拟内存是cpu假定的内存空间,只有向地址写入内容时才会为该地址分配真正的物理内存。
物理地址:物理地址是32位或36位无符号整数,是地址寄存器中存放的地址,它是与内存芯片上的单元一一对应的,可以通过它从总线上找到唯一的内存单元。
7.2 Intel逻辑地址到线性地址的变换-段式管理
段映射是从逻辑地址到线性地址的映射,用段的基地址加上逻辑地址也就是偏移量,就得到了线性地址。逻辑地址由两部分组成,前16位为段标识符,后面为偏移量。通过段标识符中的前13位可以在一个叫段描述符表的表中找到其对应的三个信息,分别是段基地址、段限长、段属性(可读可写性等)。段描述符表又有全局描述符表(GDT) 和局部描述符表 (LDT)两种。因此,通过一个逻辑地址就可以找出基地址和偏移量,算出唯一的线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
页映射是从线性地址到物理地址的映射,由cpu的页式管理单元负责将线性地址翻译成物理地址。
物理内存的最小分配单位是帧,页是虚拟内存中相当于帧的单位。线性地址的后半部分是页偏移量,而前半部分就是页号了,标志着其属于虚拟内存的哪一页。而物理内存中还有一个页表,它存储着虚拟内存页和物理内存帧的对应关系。这样,通过页号-》页表-》物理内存帧,再加上页偏移量,就得到了物理内存的地址。
7.4 TLB与四级页表支持下的VA到PA的变换
四级页表指的是虚拟页号被等分为四段,用每段页号依次查找,第一级页表中找到相应的第二级页表,以此类推。这样做的好处是节约了页表的总大小,因为如果第一级页表未分配就不用存储相应的第二级页表,节省了空间。上图以两级页表为例展示了页表不同层级之间的关系。
即使这样,页表也是庞大繁琐的,查询起来耗费时间。因此为页表建立一个缓存,称为tlb,查询过的页表项就存入tlb,下次查询时如果tlb中有就不用再依次繁琐地查找页表,节省了时间。
7.5 三级Cache支持下的物理内存访问
Cache是主存储器的缓存,它的原理是把之前调用过的主存数据缓存起来,下次如果缓存中有就不用再到主存中查找了。三级cache的原理相同,先在第一级中查找,没有再到第二级查找。采用分组的方式,固定数目的路为一组,在cache中取高位作为组号,每个组内都是一个全相联cache,一般采用最近最少用规则使被调用多的块留下来,增添效率。
7.6 hello进程fork时的内存映射
内核为子进程创建与父进程mm_struct、区域结构和页表相同的副本,将两个进程的所有页面标记为只读,区域结构标记为写时复制。这样两个进程就能读取到完全相同的虚拟内存。但进程对内存进行写操作时,由于标记了写时复制,将为这个进程复制一份新的页面,这样就与另一个进程互不干扰了。
7.7 hello进程execve时的内存映射
如图,总共分为四步:删除存在的用户区域、映射私有区域、映射共享区域、设置程序计数器。
7.8 缺页故障与缺页中断处理
总共分为三步。第一步要确认地址是否合法,如果不合法则返回段错误。第二步读写指令等是否符合该页的读写权限,如果不符合则属于非法访问,触发保护异常。若进行到第三步,说明是正常的缺页,从页表中选一个页替换为当前页,这样就更新了页表。
7.9动态存储分配管理
如图,动态内存分配器负责维护堆结构,brk指针指向堆顶。堆被动态分配器视为块的集合,块有已分配和空闲两种状态。分配器有显式和隐式,而malloc就是显式的分配方法,是应用要求释放时才能释放的。隐式则会自动释放不使用的块,称为垃圾收集。空闲的块由称为空闲链表的数据结构记录,它也有显式和隐式之分。
7.10本章小结
本章解释了c程序运行时分配存储空间的操作和过程,讲解了各种地址的翻译计算方式,说明了各级存储器是怎样互相配合的、有怎样的层级关系。最后讲解了进程如何具体地调用这些存储器的数据结构,并如何达到高效快速的效果。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:
Unix文件是一个m字节的序列。I/O设备都被视为文件,输入与输出都被视为读写文件,这样可以使所有设备的输入输出具有统一的格式和执行方式,方便管理与使用。为执行这种操作而设计的接口就是unix I/O接口。
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
1.open()函数:
功能描述:用于打开或创建文件,在打开或创建文件时可以指定文件的属性及用户的权限等各种参数。
函数原型:int open(const char *pathname,int flags,int perms)
参数:pathname:被打开的文件名(可包括路径名如"dev/ttyS0")flags:文件打开方式。
返回值:成功:返回文件描述符;失败:返回-1。
2.close()函数:
功能描述:用于关闭一个被打开的的文件。
所需头文件:#include <unistd.h>
函数原型:int close(int fd)
参数:fd文件描述符。
返回值:0成功,-1出错。
3.read()函数:
功能描述:从文件读取数据。
所需头文件:#include <unistd.h>
函数原型:ssize_t read(int fd, void *buf, size_t count);
参数:fd:将要读取数据的文件描述词。buf:指缓冲区,即读取的数据会被放到这个缓冲区中去。count: 表示调用一次read操作,应该读多少数量的字符。
返回值:返回所读取的字节数;0(读到EOF);-1(出错)。
4.write()函数:
功能描述:向文件写入数据。
所需头文件:#include <unistd.h>
函数原型:ssize_t write(int fd, void *buf, size_t count);
返回值:写入文件的字节数(成功);-1(出错)。
5.lseek()函数:
功能描述:用于在指定的文件描述符中将将文件指针定位到相应位置。
所需头文件:#include <unistd.h>,#include <sys/types.h>
函数原型:off_t lseek(int fd, off_t offset,int whence);
参数:fd;文件描述符。offset:偏移量,每一个读写操作所需要移动的距离,单位是字节,可正可负(向前移,向后移)。
返回值:成功:返回当前位移;失败:返回-1。
8.3 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;
}
、https://www.cnblogs.com/pianist/p/3315801.html
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
int getchar(void)
{
static char buf[BUFSIZ];
static char* bb=buf;
static int n=0;
if(n==0)
{
n=read(0,buf,BUFSIZ);
bb=buf;
}
return(–n>=0)?(unsigned char)*bb++:EOF;
}
Getchar调用了read函数,它从缓冲区将buf的大小读满,发生异常时再监听键盘输入读取缓冲区,读取时获得读入的字节数,如果大于0则返回缓冲区第一个字节,否则返回eof。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章解释了unix系统的io管理,列举了其io接口及相关函数,并通过hello中两个函数的具体实现作为例子展现了i/o的过程。
结论
程序源代码通过预处理、编译、汇编、链接等步骤成为可执行程序、并通过进程管理、存储管理、IO管理等系统机制在操作系统中得以运行和结束的整个过程,这样的过程被详细地称作“P2P”(From Program to Process)和“O2O”(From Zero-0 to Zero-0)。通过梳理P2P和O2O的过程,我们也串联回顾了计算机系统所学的知识,使知识结构更加清晰。
附件
列出所有的中间产物的文件名,并予以说明起作用。
9. hello.i 预处理后的源代码文件
10. hello.s hello.i编译后的汇编文件
11. hello.o hello.s汇编后的可重定位目标文件
12. hello hello.s与其他可重定位目标文件链接后的可执行程序
13. hello.txt hello.o的反汇编代码
14. hello1.txt hello的反汇编代码
15. hello.elf hello.o的elf文件
16. hello1.elf hello的elf文件
参考文献
[1] 《深入理解计算机系统》第三版,兰德尔 E.布莱恩特,大卫 R.奥哈拉伦
[2] http://www.jeepxie.net/article/29810.html
[3] x86/x86_64 CPU中逻辑地址、线性地址与物理地址 – Linux Kernel Exploration https://ilinuxkernel.com/?p=414
[4] TLB的作用及工作原理 - AlanTu - 博客园 https://www.cnblogs.com/alantu2018/p/9000777.html