计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机
学 号 120L020131
班 级 03003
学 生 黄晨
指 导 教 师
计算机科学与技术学院
2022年5月
几乎每个菜鸟程序员都编写过hello程序。或是用c语言,或是java,或是python等等。然而有没有想过,hello是如何从你编写的代码变成计算机能够实现的程序?是如何在Linux系统上运行的?
程序员在分析hello程序的一生后,方能理解Linux系统中的奥妙。
关键词:C语言;Linux;编译;进程;
目 录
2.2在Ubuntu下预处理的命令.......................................................................... - 5 -
5.3 可执行目标文件hello的格式..................................................................... - 8 -
6.2 简述壳Shell-bash的作用与处理流程...................................................... - 10 -
6.3 Hello的fork进程创建过程...................................................................... - 10 -
7.2 Intel逻辑地址到线性地址的变换-段式管理............................................. - 11 -
7.3 Hello的线性地址到物理地址的变换-页式管理....................................... - 11 -
7.4 TLB与四级页表支持下的VA到PA的变换.............................................. - 11 -
7.5 三级Cache支持下的物理内存访问........................................................... - 11 -
7.6 hello进程fork时的内存映射................................................................... - 11 -
7.7 hello进程execve时的内存映射............................................................... - 11 -
7.8 缺页故障与缺页中断处理........................................................................... - 11 -
8.1 Linux的IO设备管理方法........................................................................... - 13 -
8.2 简述Unix IO接口及其函数........................................................................ - 13 -
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P即“From Program to Process”。hello先是以hello.c的形式被程序员编写出来(Program),经过预处理后的到hello.i文本文件,然后进过编译后得到hello.s文本文件,之后再仅由汇编器汇编得到二进制文件hello.o文件,再通过链接器链接得到可执行目标文件hello,此时在终端上输入./hello命令,shell fork了一个hello的子进程,然后调用execve函数加载hello,这时hello就拥有了进程(Process)。
020即“From Zero-0 to Zero-0”。从无到被程序员编写出来,然后经过一系列编译以及被CPU执行,最后变成僵尸进程,由shell回收,又回到虚无,从无到无。
1.2 环境与工具
Linux 64.
x64CPU;2GHz;4G RAM;
gcc、edb
1.3 中间结果
hello.c源程序
hello.i预处理后的程序
hello.s编译后的程序
hello.o汇编后的程序
hello链接后的可执行目标文件
1.4 本章小结
本章简单地分析hello的一生,解释了P2P以及020,给出了环境与工具以及中间结果。
第2章 预处理
2.1 预处理的概念与作用
概念:预处理器根据以字符#开头的命令,修改原始的C程序,将hello.c文件处理成hello.i文本文件。
作用:读取并处理以字符#开头的命令,例如遇到#include<stdio.h>命令时,预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中,得到以.i作为文件拓展名的另一个C程序。总之,预处理的存在可以让编写C程序时更简洁直观。
2.2在Ubuntu下预处理的命令
图2-2-1
2.3 Hello的预处理结果解析
处理后得到了hello.c 文件
图2-3-1
打开后我们发现hello.i足足有3060行,原因是预处理器对hello.c中的包括#include<stdio.h>在内的3个#include命令进行了处理,将stdio.h, unistd.h, stdlib.h 中的命令直接插入到程序文本中。
预处理后,文本最后一段:
图2-3-2
可以看到这一段与原始.c文件主体部分并无区别。预处理器只是执行了插入工作。
2.4 本章小结
预处理器对我们原始.c程序中的#命令进行了处理,极大地方便了我们的编程工作。如果不进行预处理,每次写.c程序时都要手动导入重复指令,费时费力;可以说,预处理步骤在某种意义上极大地提高了程序员的编程效率。
第3章 编译
3.1 编译的概念与作用
概念:编译器将文本文件hello.i翻译成文本文件hello.s。
作用:编译器遵循一定规则,经过语义检查和优化后,将高级语言转化成汇编语言。
3.2 在Ubuntu下编译的命令
图3-2-1
3.3 Hello的编译结果解析
3.3.1数据
(1)常量
字符串常量"用法: Hello 学号 姓名 秒数!\n" 存放在此处,标识符为.LC0:
图3-3-1
字符串常量"Hello %s %s\n" 存放在此处,标记符为.LC1:
图3-3-2
(2)变量
变量i存放在%eax中:
图3-3-3
argc最开始存放在%edi中:
图3-3-4
同理argv开始存放在%rsi中:
图3-3-5
3.3.2操作
(1)赋值
以给局部变量i赋值为例,使用movl指令给i赋予初值0
图3-3-6
(2)类型转换
atoi函数将字符串转换为整型,编译器实现:
图3-3-7
返回值存放在%eax中,被赋予给%edi。
(3)算数操作
i++这一操作实现如下:
图3-3-8
使用add让存放在%rbq-4处的值加1即可。
(4)关系操作
以 argc!=4 为例,实现如下:
图3-3-9
先使用cmp比较argc和4,利用je,如果相等的话直接跳到.L2处,如果不相等则不跳跃继续执行后续命令:
图3-3-10
i<8类似,将je换为jle,让7大于等于i时跳转:
图3-3-11
(5)数组/指针操作
数组访问采用基址+偏移量等方式寻址,如访问argv[2]时过程如下:
图3-3-12
先将argv的地址-32(%rbp)赋予%rax,然后加上16作为偏移量,再将%rax指向的值赋给%rdx,这样%rdx就保存了argv[2]的值。
指针访问类似数组访问。
(6)控制转移
if在上文关系操作中已经提到,for循环其实就是上文算数操作加上关系操作的组合,上文也有提到,即先给i赋予初值0,判断是否小于8(7是否大于等于i),如果是则由jle跳转到.L4处(每次循环时i都会加1),重复执行直到判定为否然后向下接着执行命令:
图3-3-13
(7)函数操作
函数优先使用%edi/%rdi传递参数,如调用printf函数前使用mov指令将$.LC1中存储的字符串赋予%rdi:
图3-3-14
接着使用call调用函数,如调用atoi函数:
图3-3-15
返回值用%eax接收,如atoi的返回值用%eax接收后,赋予%edi给sleep函数使用:
图3-3-16
3.4 本章小结
编译步骤使高级程序语言编程汇编语言,同时对代码经行优化;但由于优化时编译器会“考虑”到一些意外情况,常常使优化看起来不是那么“优秀”,有时甚至比人工实现汇编语言还要复杂。
但总的来说,将高级语言转化成汇编语言后,能够更容易地将其转换成计算机能够识别的二进制机器语言,但代价是汇编语言难以移植到其他机器上,因为汇编语言是为特定的处理器设计的。
第4章 汇编
4.1 汇编的概念与作用
概念:汇编器将程序翻译成机器语言。
作用:将汇编语言翻译成机器能够识别并执行的二进制机器语言,使程序执行成为可能。
4.2 在Ubuntu下汇编的命令
4.3 可重定位目标elf格式
hello.o的ELF格式由以下几个部分组成:
(1)ELF头:
(2)节头部表:
(3)符号表:
(4)重定位条目:
4.4 Hello.o的结果解析
使用objdump -d -r hello.o指令,得到反汇编:
可以发现之前的10进制数全部换成了16进制数;分支转移时,汇编语言使用.L2作为操作数,而机器语言反汇编后发生了变化:
可以发现寻址方式发生了不同;同样的,函数调用时,汇编语言直接调用函数名,而机器语言反汇编后发生了变化:
机器语言反汇编后基本与汇编语言相同,不同的是寻址方式发生了改变,代码格式也发生了变化,如call变成了callq。
4.5 本章小结
汇编器将.s文本文件编译成.o二进制文件,使得机器能够识别我们编写的程序。汇编器生成的可重定位目标文件包含了二进制代码和数据,可以在编译时与其他可重定位目标文件合并,创建一个可执行目标文件,这为下一步链接提供了基础。
第5章 链接
5.1 链接的概念与作用
概念:将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。
作用:链接器使得分离编译成为可能,即不需要将一个大型应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。
5.2 在Ubuntu下链接的命令
(以下格式自行编排,编辑时删除)
使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
使用命令:
找到gcc实际包含的命令行:
使用ld链接命令链接包含hello.o文件在内的部分:
5.3 可执行目标文件hello的格式
(1)ELF头:
(2)节头部表:
(3)符号表:
5.4 hello的虚拟地址空间
使用edb加载hello,发现_init节的起始地址是0x00401000:
与5.3对照,发现节头表中_init节也是从0x00401000开始:
接下来的如.plt节从0x00401020开始,5.3节头表中.plt也从0x00401020开始:
其余同理。
5.5 链接的重定位过程分析
使用objdump -d -r hello指令,部分代码如下:
可以发现不同处:
hello.o中地址占位符被改为正确的地址,即已完成了重定位:
此外,hello.o中没有包含的函数代码,在hello中得到添加:
重定位过程:
(1)重定位位节和符号定义。链接器将所有同类型的节合并成同一类型的新的聚合节。
(2)重定位位节中的符号引用。链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址。正如上文提到的,之前hello.o中地址占位符通过重定位公式被替换成了正确的地址。
5.6 hello的执行流程
子程序名:
加载hello到_start:
ld-2.27.so!_dl_start
ld-2.27.so!_dl_init
hello!_start
从_start到call main:
libc-2.27.so!__libc_start_main
libc-2.27.so!__cxa_atexit
libc-2.27.so!__libc_csu_init
libc-2.27.so!_setjmp
hello!main
main内部:
hello!puts@plt
hello!exit@plt
hello!printf@plt
hello!sleep@plt
hello!getchar@plt
到程序终止:
ld-2.27.so!_dl_runtime_resolve_xsave
ld-2.27.so!_dl_fixup
ld-2.27.so!_dl_lookup_symbol_x
libc-2.27.so!exit
5.7 Hello的动态链接分析
(以下格式自行编排,编辑时删除)
分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。
在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为它的共享模块在运行时可以加载到任意位置。正确的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。GNU使用延迟绑定将过程地址的绑定推迟到第一次调用该过程,而延迟绑定是通过GOT和PLT实现的。GOT是数据段的一部分,而PLT是代码段的一部分。下面我们重点关注GOT:
GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。
观察GOT表的变化:
上文我们已经知道got的起始地址为0x404000,在内存中找到这个位置:
可以发现部分为空,而当dl_init执行后,变成了:
明显发生了变化。
5.8 本章小结
本章实现了链接操作,链接器使得分离编译成为可能,即不需要将一个大型应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。
我们将hello.o二进制程序通过链接变成了hello可执行程序,同时分析了其elf信息、重定位过程,自此hello的编译工作基本完成。
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程是一个执行中的程序的实例,每一个进程都有它自己的地址空间,一般情况下,包括文本区域、数据区域、和堆栈。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储区着活动过程调用的指令和本地变量。
作用:每个进程进程都会占用CPU运行的一个时间片,这个时间片由操作系统来分配,这样每个进程看上去都好像在独自占用CPU一样,这种机制使得在一台机器上运行多个程序成为可能。
6.2 简述壳Shell-bash的作用与处理流程
作用:代表用户执行进程,交互性地解释和执行用户输入的命令,能够通过调用系统级的函数或功能执行程序、建立文件、进行并行操作等。同时也能够协调程序间的运行冲突,保证程序能够以并行形式高效执行。bash还提供了一个图形化界面,提升交互的速度。
处理流程:首先读取一行命令,检查命令是否正确,将命令分割后检查是否是内置命令,如果不是内置命令,则调用fork()创建子进程。
6.3 Hello的fork进程创建过程
输入./hello 120L020131 黄晨 1时,shell对输入的命令行进行解析,发现不是内置的系统命令(quit等),于是shell调用fork()创建了一个子进程。
子进程得到与父进程完全相同但是独立的一个副本。
6.4 Hello的execve过程
当创建了一个子进程后,子进程调用exceve函数在当前子进程的上下文加载并运行新的hello程序,加载并执行:
(1)删除已存在的用户区域:删除当前进程虚拟地址的用户部分中已存在的区域结构。
(2)映射私有区域:为新程序的代码、数据、bss和栈区域创建新的区域结构。所有这些区域结构都是私有的,写时复制的。虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区。bss区域是请求二进制零的,映射匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零。
(3)映射共享区域:如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域。
(4)设置程序计数器(PC):exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。下一次调用这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。
6.5 Hello的进程执行
(以下格式自行编排,编辑时删除)
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象比如描述地址空间的页表,包含当前进程有关信息的进程表,以及包含进程以打开文件的信息的文件表构成。在进程执行到某些时刻,内核可以决定抢占当前进程,并开始一个之前被抢占的进程开始执行,这种决策就叫做调度,由调度器来决定,在内核调度了一个新的进程后就会发生上下文切换的操作。
进程时间片时操作系统分配给每个正在运行的进程微观上的一段CPU时间。
具体到hello,当执行hello程序时,控制流在hello内处于用户模式;系统调用sleep时,CPU转为内核态,并将hello进程从运行队列加入等待队列,然后CPU去执行其他的用户进程。若sleep过程没有被中断,当定时器结束时,内核发送一个中断信号,CPU又转为内核态,将hello重新加入运行队列。
6.6 hello的异常与信号处理
hello在执行中会出现异常:中断(Interrupts)、陷阱(Traps)、故障(Faults)、终止(Aborts)。产生的信号如下:
中断、陷阱处理程序将控制返回到下一条指令,故障处理程序要么重新执行引起故障的指令(已修复),要么终止,终止则直接中止当前程序。下面以实际运行hello为例
正常运行:
不停乱按:
回车:
ctrl z:
ctrl z后ps :
ctrl z后jobs:
ctrl z后pstree(部分):
ctrl z后fg:
再次 ctrl z后:
ctrl z 后kill:
再次运行hello,然后ctrl c:
6.7本章小结
本章介绍了shell-bash的作用与处理流程,并以hello为例,描述了hello从fork到终止的全过程,让我们更加清晰地认识到了程序是如何在Linux系统上运行的。
“异常是再正常不过的事了”,这句话确实有其道理(虽然我觉得Exception翻译成例外会比较好)。不过异常同样保证了硬件的操作正常以及hello进程的正确运行,具有积极的一面。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:在有地址变换功能的计算机中,访问指令给出的地址(操作数)叫逻辑地址。例如hello中mov操作数[rbq-0x20]:
线性地址:逻辑地址到物理地址变换的中间层,上述逻辑地址的基地址加上偏移地址就是线性地址。
虚拟地址:计算机呈现出比实际拥有的内存大得多的内存量。因此它允许程序员编制并运行比实际系统拥有的内存大得多的程序。如hello中0x00401217:
物理地址:CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
首先根据段选择符的TI部分判断需要用到的段选择符表是全局描述符表还是局部描述符表,TI=0时使用全局描述符表,TI=1时使用局部描述符表。其余的高13位用以确定当前的段描述符在描述符表中的位置,然后从中取出32位的段基址地址,将其与32位的段内偏移量相加得到32位的线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组,磁盘上数组的内存被缓存在物理内存中。于是虚拟内存中的数据在物理内存中被分割成固定大小的块,每个块称为一个“页”。
“页表”是一个页表条目的数组,它将虚拟页地址映射到物理页地址。linux系统中每个进程都有一套页表。通过此映射可以将线性地址变换为物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
使用四级页表层次结构的地址翻译过程如下:
虚拟地址被划分为4个VPN和1个VPO。每个VPN i都是一个到第i级页表的索引,(1≤i≤4).第j级页表中的每个PTE(1≤j≤k-1),都指向第j+1级的某个页表的基址。第4级页表中的每个PTE包含某个物理页面的PPN,或者一个磁盘块的地址。为了构造物理地址,在能够确定PPN之前,MMU必须访问4个PTE:
7.5 三级Cache支持下的物理内存访问
当处理器需要访问某地址的数据时,先从1级Cache中取数,如果未命中则用该地址到2级Cache中取数,并将数据加载到1级Cache中,如果2级Cache也没有命中则从3级Cache中取数并将数据加载到2级Cache中。如果此时仍未命中,则从主存中读取数据。
7.6 hello进程fork时的内存映射
使用fork函数时,内核为hello创建各种数据结构,并分配PID。为了给hello创建虚拟内存,内核又创建hello进程的mm_struct、区域结构和页表的副本。fork刚完成时,两个进程的内存内容相同,并且两者的虚拟地址都被映射到了相同的物理地址。
7.7 hello进程execve时的内存映射
内存映射包含如下操作:
1.删除已存在的用户区域;
2.映射私有区域,包含hello的.text段、.data段、.bss段和堆栈段新创建的区域结构等;
3.映射共享区域,包含由动态链接映射到hello进程的共享对象。
4.设置程序计数器,使之指向hello的入口点。
7.8 缺页故障与缺页中断处理
缺页故障:DRAM缓存不命中称为缺页。缺页故障由缺页处理程序处理,地址翻译硬件从内存处读出相应PTE判断出虚拟页所在位置,然后调用缺页处理程序,该程序会选择一个牺牲页,更新PTE,然后返回并重新启动导致缺页的指令,虚拟地址就被重新发送到地址翻译硬件。
7.9动态存储分配管理
动态内存分配管理由动态内存分配器实现;动态内存分配器维护着一个进程的虚拟内存区域(堆)。分配器将堆视为一组不同大小的块的集合来维护,每个块就是一个连续的虚拟内存片,要么是已分配的,显式地保留为供应用程序使用;要么是空闲的,保持空闲直到它显式地被应用所分配。一个已分配的块保持已分配状态直到它被释放。
分配器有两种基本风格。两种风格都要求应用显式地分配块。
显式分配器:要求应用显式地释放任何已分配的块,如printf函数调用的malloc函数分配一个块,并调用free函数来释放一个块。
隐式分配器:要求分配器检测一个已分配块何时不再被程序所使用,然后释放这个块。
7.10本章小结
本章阐述了hello的储存管理以及几种地址的区别以及相互转换。给出了四级页表的VP到PA的变换以及三级Cache下的物理内存访问。同时阐述了fork和execve时的内存映射。最后阐述了缺页故障与缺页中断处理以及动态存储分配管理。
一个系统中的进程是与其他进程共享CPU和主存资源的。这可能会导致进程需要太多内存时无法运行,但引入虚拟内存、动态存储后很好地解决了这个问题。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
一个Linux文件就是一个m字节的序列,所有的IO设备都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix IO。
8.2 简述Unix IO接口及其函数
(1)打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个IO设备。内核返回描述符(一个小的非负整数),它在后续对此文件的所有操作中识别这个文件。
函数: #include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int open(char *filename, int flags, mode_t mode);
//若成功返回新文件描述符,若出错为-1
(2)读文件。读操作就是从文件复制n个字节到内存,从当前文件位置k开始,然后将k增加到k+n。
函数: #include <unistd.h>
ssize_t read(int fd, void *buf, size_t n);
//若成功返回读的字节数,若EOF则为0,若出错为-1。
(3)写文件。与读文件类似,写操作就是从内存复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
函数: #include <unistd.h>
ssize_t write(int fd, const void *buf, size_t n);
//若成功返回写的字节数,若出错为-1。
(4)关闭文件。当应用完成了对文件的访问后,它就通知内核关闭这个文件。
函数: #include <unistd.h>
int close(int fd);
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;
}
其中va_list被定义为char *,说明它是一个字符指针。
再看i = vsprintf(buf, fmt, arg);这句,vsprintf的源码是:
int vsprintf(char *buf, const char *fmt, va_list args)
{
char* p;
char tmp[256];
va_list p_next_arg = args;
for (p=buf;*fmt;fmt++) {
if (*fmt != '%') {
*p++ = *fmt;
continue;
}
fmt++;
switch (*fmt) {
case 'x':
itoa(tmp, *((int*)p_next_arg));
strcpy(p, tmp);
p_next_arg += 4;
p += strlen(tmp);
break;
case 's':
break;
default:
break;
}
}
return (p - buf);
}
返回的是要打印出来的字符串的长度,即vsprintf接受确定输出格式的格式字符串fmt,用格式字符串对个数变化的参数进行格式化,产生格式化输出。然后通过write(buf, i)把目标字符串打印到终端。
在write函数中,将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址,int INT_VECTOR_SYS_CALLA代表通过系统调用syscall。
syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。
字符显示驱动子程序将通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中。
显示芯片会按照一定的刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
于是打印字符串就显示在了屏幕上。
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
(以下格式自行编排,编辑时删除)
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
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;
}
返回值是用户输入的第一个字符的ASCII码,如果出错则返回EOF。
8.5本章小结
本章介绍了Linux系统提供的基本IO服务,以及printf、getchar函数的实现。
结论
hello从原始的高级程序语言逐渐变为汇编语言、二进制系统语言、可执行程序,背后蕴藏着进程管理、存储管理、IO管理等各种知识;可以说,弄清楚了hello的“生命周期”,就半只脚踏入了计算机系统高手的大门。
在学习大黑书(《深入理解计算机系统》)和本课程后,我才发现计算机的世界是如此的精彩;只不过,大黑书为了能够让更多人即没有太多基础的学生学懂,在很多地方进行了简化;我想,如果带着那些被简化后的知识再次学习,又会有怎样的感受呢?我的一个设想:将计算机系统这门课分为上与下,上部分浅尝辄止,下部分则结合已有知识深入讲解。
附件
hello.c源程序
hello.i预处理后的程序
hello.s编译后的程序
hello.o汇编后的程序
hello链接后的可执行目标文件
参考文献
[1] 深入理解计算机系统(Computer Systems A Programmer’s Perspective) Third Edition. Randal E.Bryant, David R.O’Hallaron