我是Hello,我是每一个程序猿¤的初恋(羞羞……)
却在短短几分钟后惨遭每个菜鸟的无情抛弃(呜呜……),他们很快喜欢上sum、sort、matrix、PR、AI、IOT、BD、MIS……,从不回头。
只有我自己知道,我的出身有多么高贵,我的一生多么坎坷!
多年后,那些真懂我的大佬(也是曾经的菜鸟一枚),才恍然感悟我的伟大!
想当年: 俺才是第一个玩 P2P的: From Program to Process
懵懵懂懂的你笨笨磕磕的将我一字一键敲进电脑存成hello.c(Program),无意识中将我预处理、编译、汇编、链接,历经艰辛,我-Hello一个完美的生命诞生了。
你知道吗?在壳(Bash)里,伟大的OS(进程管理)为我fork(Process),为我execve,为我mmap,分我时间片,让我得以在Hardware(CPU/RAM/IO)上驰骋(取指译码执行/流水线等);
你知道吗?OS(存储管理)与MMU为VA到PA操碎了心;TLB、4级页表、3级Cache,Pagefile等等各显神通为我加速;IO管理与信号处理使尽了浑身解数,软硬结合,才使我能在键盘、主板、显卡、屏幕间游刃有余, 虽然我在台上的表演只是一瞬间、演技看起来还很Low、效果很惨白。
感谢 OS!感谢 Bash!在我完美谢幕后回收了我。 我赤条条来去无牵挂!
我朝 CS(计算机系统-Editor+Cpp+Compiler+AS+LD + OS + CPU/RAM/IO等)挥一挥手,不带走一片云彩! 俺也是 O2O: From Zero-0 to Zero-0。
历史长河中一个个菜鸟与我擦肩而过,只有CS知道我的生、我的死,我的坎坷,“只有 CS 知道……我曾经……来…………过……”————未来一首关于Hello的歌曲绕梁千日不绝 !!
本文章主要内容是hello.c文件在linux下的从产生到结束的全过程。首先是对代码的预处理,编译,汇编的过程,然后是对hello的动态链接。还包括了进程管理、存储管理、I/O管理,存储层次结构、异常控制流、虚拟内存。通过hello实例的学习,可以更深入地了解计算机系统底层运行机制。
关键词:编译、存储层次结构、链接、虚拟内存、操作系统、异常控制流、信号
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
在linux的环境下,经过预处理得到hello.i文件,编译形成hello.s汇编语言文件,汇编将其转化为hello.o可重定位目标程序二进制文件,链接最终成为一个可执行文件hello
P2p:
执行目标文件,shell使用fork形成子进程,分配内存资源,execve映射虚拟内存进入程序中,进入main函数执行目标代码,通过取指、译码、执行等微程序,逐步执行目标文件中的程序
020:
程序运行结束,shell父进程回收子进程,内核删除相关的数据结构
1.2 环境与工具
1.2.1 硬件环境
X64 CPU;2.5GHz;16G RAM;256GHD Disk 以上
1.2.2 软件环境
Windows 10; Ubuntu 18.04 ;
1.2.3 开发工具
gpedit+gcc,vim,wxhexeditor
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
文件名 作用
hello.c hello源代码
hello.i 预处理文本文件
hello.s 编译后的汇编文件
hello.o 汇编后的可重定位文件
hello 链接后的可执行文件
hello.txt hello.o的反汇编代码
1.4 本章小结
这里介绍了hello从源代码到执行的流程,包括预处理,编译,汇编,链接等过程,体现了p2p 020的理念
2.1 预处理的概念与作用
预处理指在代码翻译的过程中生成二进制代码之前的过程。
预处理会展开以#起始的行解释为预处理指令
#if/#ifdef/#ifndef/#else/#elif/#endif、#define、#include等
include形式声明的文件复制到新的程序中,define定义的字符串用实际值代替。
预处理可以让目标程序变小,提高运行速度
2.2在Ubuntu下预处理的命令
图2-1 预处理命令
gcc hello.c -E -o hello.i
生成的与处理之后的文本文件
图2-2 预处理文本文件
2.3 Hello的预处理结果解析
经过了预处理,hello.c转化为hello.i,hello.i程序被拓展为3118行,但依旧是c语言文本文件,从3100行开始就是原始代码,原文件中的头文件依次展开,形成一个完备的程序文本
图2-3 预处理文本文件末尾源代码
2.4 本章小结
hello.c中的源代码很短小,但是真正的实现需要很多附加成分,经过了预处理指令,下一阶段就要进行汇编处理
3.1 编译的概念与作用
编译指的是将高级语言程序代码翻译成为等价的汇编语言代码的过程。
这里编译器将文本文件hello.i翻译成文本文件hello.s
3.2 在Ubuntu下编译的命令
图3-1 编译指令
gcc hello.i -S -o hello.s
生成的hello.s文件,图·3-2,3-3
图3-2 hello.s(1)
图3-3 hello.s(2)
3.3 Hello的编译结果解析
3.3.1数据
包括一个全局变量和两个字符串
两个字符串都在只读数据节
如图3-4,Usage: Hello 学号 姓名!\n ,printf的输出格式化参数,其中编码为utf8
Hello %s %s\n,printf传入的输出格式化参数
图3-4 两个字符串
全局变量
图3-5 sleepsecs变量
整型sleepsecs,源代码中的数据为2.5,在.data节中将其声明为long型,隐式类型转换值为2,编译器首先将sleepsecs在.text代码段中声明为全局变量,对齐方式是4,大小是4个字节
其他数据在汇编代码中体现。
3.3.2赋值
程序中涉及的赋值有两处
第一处就是全局变量sleepsecs赋值为2.5
另一处就是对i进行赋值
图3-6
图3-7
3.3.3类型转换
程序中涉及到一处隐式类型转换
全局变量sleepsecs是一个整型变量,而初始化的值是2.5,是一个浮点数类型,此时发生类型转换,向下取整,获得新值2
3.3.4算数操作
图3-8
该汇编代码对应i++,-4(%rbp)对应i
3.3.5关系操作
两处判断
第一处判断argc是否等于3
图3-9
图3-10
-20(%rbp)对应argc
第二处判断i是否小于10
图3-11
图3-12
-4(%rbp)对应i
3.3.6数组操作
程序中的数组操作主要是对于输入参数的输出
图3-13
图3-14
3.3.7控制转移
程序包含两处控制转移
第一处是判断输入参数个数是否等于3,如果等于3才进行跳转
图3-15
图3-16
第二处是判断循环体是否结束
图3-17
图3-18
i作为计数变量,循环十次输出字符串,当i<=9是都继续输出,,大于则循环结束
3.3.8函数操作
程序包括五个函数
4.1 汇编的概念与作用
汇编器就是将.s汇编程序翻译成机器语言指令,把汇编语言书写的源程序输出成为机器语言表示的目标程序,基本上是一一对应的关系。将指令打包为可重定位目标程序保存在.o文件中
4.2 在Ubuntu下汇编的命令
图4-1 汇编指令
gcc hello.s –c –o hello.o
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
在文件头中得到节头表的信息,然后再使用节头表中的字节偏移信息得到各节在文件中的起始位置,以及各节所占空间的大小。代码段是可执行的,但是不能写;数据段和只读数据段都不可执行,而且只读数据段也不可写
图4-2 hello.o的文件头信息
hello.o的文件头信息,信息说明这个文件是可重定位文件,有13个节
图4-3 节头信息
查看13个节头信息
查看符号表信息
4.4 Hello.o的结果解析
objdump -d -r hello.o > hello1.txt获得反汇编代码
差别:
图4-5
而反汇编文件中的目标地址是下一条指令,因为调用的都是共享库中的函数,需要连接之后才能确定函数执行地址。
相同的,全局变量也获得了确定的地址而不是段名称+%rip
![在这里插入图片描述](https://img-blog.csdnimg.cn/20181230192756623.png)
图4-6
4.5 本章小结
hello.s与hello.o的反汇编文件大体上一致,不同之处就是分支跳转变为确定的地址;函数调用跟着确定的指令,全局变量也跟着确定的偏移量。反汇编代码的栈空间利用率较高,由本章可以知道从汇编语言到机器语言过程中所需要的必要的转换
5.1 链接的概念与作用
链接是将各种代码和数据段收集并组合成为一个单一文件的过程,这个文件可以被加载到内存并执行。链接可以执行于编译时,也就是源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至执行于运行时,由应用程序来执行。
链接器在软件开发时扮演着一个关键的角色,因为它们使得分离编译成为可能。
5.2 在Ubuntu下链接的命令
图5-1 链接命令
使用ld的链接命令,链接hello.o crt1.o,crti.o,crtn.o,libc.so
产生一个可执行文件,链接的目标文件是64位的
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
图5-2 hello的文件头
hello的elf文件,由图5-4,5-5可以得到各段的基本信息,起始地址大小等,作为一个可执行文件,每段的起始地址对应虚拟内存中的虚拟地址。数据段可只读数据段不可执行,只读数据段和代码不可写
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
在0x400000~0x401000段中,程序被载入,自虚拟地址0x400000开始,自0x400fff结束
PHDR:保存程序头表。
INTERP:指定在程序已经从可执行文件映射到内存之后,必须调用的解释器
LOAD:表示一个需要从二进制文件映射到虚拟地址空间的段。其中保存了常量数据(如字符串)、程序的目标代码等。
DYNAMIC:保存了由动态链接器使用的信息。
NOTE:保存辅助信息。
GNU_STACK:权限标志,标志栈是否是可执行的。
GNU_RELRO:指定在重定位结束之后那些内存区域是需要设置只读。
图5-6 程序头表
5.5 链接的重定位过程分析
objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
使用objdump -d -r hello > helloobjdump.txt
反汇编文件相比于hello.o有很多不同,反汇编文件中给出了重定位的结果,确定了虚拟地址,hello.o文件的地址是从0开始的
图5-7 反汇编文件结果
图5-8 hello中多了很多节
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
函数执行流程
ld-2.27.so!_dl_start ld-2.27.so!_dl_init hello!_start
libc-2.27.so!__libc_start_main
-libc-2.27.so!__cxa_atexit
-libc-2.27.so!__libc_csu_init hello!_init libc-2.27.so!_setjmp
-libc-2.27.so!_sigsetjmp
–libc-2.27.so!__sigjmp_save hello!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的动态链接分析
对于动态链接库中的函数,编译器不知道函数运行时的地址,就添加重定位记录,等待动态链接器处理,延迟绑定。在形成可执行文件时,还是需要用到动态链接库。在形成可执行程序时,发现引用了一个外部的函数,此时会检查动态链接库。在dl_init调用之前,对于每一条PIC函数调用,调用的目标地址都实际指向PLT中的代码逻辑,GOT存放的是PLT中函数调用指令的下一条指令地址。在dl_init调用之后,GOT[1]指向重定位表(依次为.plt节需要重定位的函数的运行时地址)用来确定调用的函数地址, GOT[2]指向动态链接器ld-linux.so运行时地址。
5.8 本章小结
本章主要介绍了链接的概念和作用,hello的elf格式,hello的反汇编文件与hello.o的区别。linux通过此过程产生了一个可以执行的二进制文件
6.1 进程的概念与作用
进程就是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态构成的。包括程序的代码和数据、栈、通用目的寄存器的内容、程序计数器、环境变量和打开文件描述符的集合
进程给我们一个假象,就是我们的程序是当前系统运行的唯一程序一样。我们的程序独占处理器和内存,处理器好像无间断的一条一条执行我们程序中的指令。我们的程序中的代码和数据好像是系统内存中唯一的对象
6.2 简述壳Shell-bash的作用与处理流程
Shell俗称壳(用来区别于核),是指“为使用者提供操作界面”的软件(命令解析器)。它接收用户命令,然后调用相应的应用程序。Shell 是指一种应用程序,这个应用程序提供了一个界面,用户通过这个界面访问操作系统内核的服务。
1.初始化各个变量
2.解析命令行,把命令和参数分离,解析
3.如果是内置命令直接执行
4.如果不是内置命令,阻塞信号,创建子进程
5.父进程将子进程加入joblist中
6.判断是否是后台运行,后台只打印消息,前台等待完成
6.3 Hello的fork进程创建过程
linux运行的终端程序会对输入的命令行进行解析,因为hello不是一个内置的shell命令所以解析之后终端程序判断输入的命令是执行当前文件夹的可执行文件hello。
终端程序调用fork创建一个新的子进程。新创建的子进程几乎但不完全与父进程相同,子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,这就意味着,当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程与子进程之间最大的区别在于它们拥有不同的PID。
6.4 Hello的execve过程
当fork之后,子进程调用execve函数,execve函数加载并运行可执行目标文件filename(hello),并且带有参数列表argv和环境变量列表envp。
参数列表:argv变量指向一个以null结尾的指针数组,其中每个指针都指向一个参数字符串,并且按照惯例,argv[0]是可执行文件的名字,环境变量的列表是又一个类似的数据结构表示。
execve加载了hello以后,调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数
6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
时间片就是进程执行控制流的那一段时间
上下文就是内核重新启动一个被抢占的进程所需的状态,包括通用目的寄存器,浮点寄存器,程序计数器,用户栈,状态寄存器,内核栈和各种内核数据结构
hello初始运行在用户模式,在hello进程调用sleep之后切换至内核模式,内核处理休眠请求主动释放当前进程,并将hello进程从运行队列中移出加入等待队列, 内核抢占当前进程,开始一个其他的进程,抢占当前进程,并使用上下文切换的机制将控制权转移至新的进程sleep中
当定时器到2秒发送一个中断信号,此时进入内核状态执行中断处理,将hello进程从等待队列中移出重新加入到运行队列,成为就绪状态,hello进程就可以继续进行自己的控制逻辑流了。
6.6 hello的异常与信号处理
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
图6-1 成功运行结果
图6-1,6-2是正常运行时的结果,输出10次每次间隔2秒,结束销毁进程
图6-3是在运行的时候键入^z,发送一个 SIGTSTP 信号给前台进程组中的进程,从而将其挂起,但是进程没有被销毁,fg 1可以继续这个进程
图6-4是在运行的过程中键入^C,会向当前进程发送一个SIGINT信号,从而使当前进程中断,结束hello,并回收hello进程
图6-5 ^z情况下kill
图6-5在运行的时候键入^z,发送一个 SIGTSTP 信号给前台进程组中的进程,从而将其挂起,但是进程没有被销毁,此时kill这个hello进程,hello被回收
6.7本章小结
本章阐述了进程的定义作用,还有进程需要的上下文,shell处理的流程,fork创建新进程的过程,execve执行的过程,异常与信号处理,内核和用户模式的切换等。
7.1 hello的存储器地址空间
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
物理地址(physical address)
用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相相应。
虚拟内存(virtual memory)
这是对整个内存(不要与机器上插那条对上号)的抽像描写叙述。
它是相对于物理内存来讲的,能够直接理解成“不直实的”,“假的”内存。由于现代操作系统都提供了一种内存管理的抽像,即虚拟内存(virtual memory)。进程使用虚拟内存中的地址,由操作系统协助相关硬件,把它“转换”成真正的物理地址。
有了这种抽像。一个程序,就能够使用比真实物理地址大得多的地址空间。
逻辑地址(logical address)
Intel为了兼容,将远古时代的段式内存管理方式保留了下来。
逻辑地址指的是机器语言指令中,用来指定一个操作数或者是一条指令的地址。
线性地址(linear address) 在csapp中也叫虚拟地址(virtual address)
跟逻辑地址类似,它也是一个不真实的地址,hello.s中使用的就是虚拟空间的虚拟地址假设逻辑地址是相应的硬件平台段式管理转换前地址的话,那么线性地址则相应了硬件页式内存的转换前地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
1、逻辑地址=段选择符+偏移量
2、每个段选择符大小为16位,段描述符为8字节(注意单位)。
3、GDT为全局描述符表,LDT为局部描述符表。
4、段描述符存放在描述符表中,也就是GDT或LDT中。
5、段首地址存放在段描述符中。
每个段的首地址都存放在自己的段描述符中,而所有的段描述符都存放在一个描述符表中(描述符表分为全局描述符表GDT和局部描述符表LDT)。而要想找到某个段的描述符必须通过段选择符才能找到。
段选择符由三个部分组成,从右向左依次是RPL、TI、index(索引),index部分。我们可以将描述符表看成是一个数组,每个元素都存放一个段描述符,那index就表示某个段描述符在数组中的索引。
图7-1 虚拟内存的数据结构
7.3 Hello的线性地址到物理地址的变换-页式管理
内存系统中有TLB,页表,高速缓存。
虚拟地址分成VPN和VPO两个部分
物理地址分为PPN和PPO两部分
TLB利用VPN的数据进行虚拟寻址,VPN高位为标记TLBT,低位为索引TLBI
开始时,MMU从虚拟地址中抽取VPN,在TLB中寻找,如果命中直接返回,如果不命中,再查看页表命中返回PPN。
PPN和PPO组合成了物理地址,其中PPO有VPO直接获得。
物理地址再分为CT,CI,CO三个部分
7.4 TLB与四级页表支持下的VA到PA的变换
如果TLB中没有命中,MMU向页表中查询,CR3确定第一级页表的起始地址,VPN1(9bit)确定在第一级页表中的偏移量,查询出PTE,确定第二级页表的起始地址,VPN2(9bit)确定在第一级页表中的偏移量,查询出PTE,确定第三级页表的起始地址,VPN3(9bit)确定在第一级页表中的偏移量,查询出PTE,确定第四级页表的起始地址,VPN4(9bit)确定在第一级页表中的偏移量,查询出PTE,如果在物理内存中且权限符合,确定第二级页表的起始地址,以此类推,最终在第四级页表中查询到PPN,与VPO(12bit)组合成物理地址
7.5 三级Cache支持下的物理内存访问
在上一步中我们已经获得了物理地址,使用CI进行组索引,每组8路,对8路的块分别匹配CT,如果匹配成功且块的valid标志位为1,则命中,根据数据偏移量CO取出数据返回。
如果没有匹配成功或者匹配成功但是标志位是1,则不命中(miss),向下一级缓存中查询数据。查询到数据之后,一种简单的放置策略如下:如果映射到的组内有空闲块,则直接放置,否则组内都是有效块,产生冲突(evict),则采用最近最少使用策略进行替换。
7.6 hello进程fork时的内存映射
当fork函数被系统调用时,内核会为hello创建子进程,同时会创建各种数据结构并分配给hello唯一的PID。为了给hello创建虚拟内存,
内核创建了当前进程的mm_struct、vm_area_struct和页表的原样副本
并将两个进程中的每个页面都标记为只读
将两个进程中的每个区域结(vm_area_struct)构都标记为写时复制(cow)。
在新进程中返回时,新进程拥有与调用fork进程相同的虚拟内存
随后的写操作通过写时复制机制创建新页面
7.7 hello进程execve时的内存映射
图7-4 内存映射
execve函数在当前进程中加载并运行新程序的步骤
删除已存在的用户区域
创建新的区域结构
代码和初始化数据映射到.text和.data区(目标文件提供)
.bss和栈映射到匿名文件
设置pc,指向代码区域入口点
linux根据需要换入代码和数据页面
7.8 缺页故障与缺页中断处理
缺页中断处理:缺页处理程序是系统内核中的代码,选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令再次发送VA到MMU,MMU就能正常翻译VA
图7-5 缺页
7.9动态存储分配管理
7.9.1空闲链表
图7-6 隐式空闲链表
隐式空闲链表,通过头部中的大小字段隐含地连接空闲块
图7-7 显式空闲链表
显式空闲链表,通过空闲块中的指针连接空闲块
7.9.2合并空闲块
图7-9 合并空闲块
利用头部和脚步的块大小找到前后的空闲块,并且合并
7.10本章小结
本章主要介绍了hello的存储器地址空间、intel的段式管理、hello的页式管理,以intel Core7在指定环境下介绍了虚拟地址到物理地址的变换、物理内存访问,还介绍了hello进程fork时的内存映射、execve时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。
8.1 Linux的IO设备管理方法
所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
while( (n = read(STDIN_FILENO, buf, BUFFSIZE)) != n)
{
if(write(STDOUT_FILENO, buf, n) != n)
perror("write error");
}
8.3 printf的实现分析
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;
}
arg获得第二个不定长参数,即输出的时候格式化串对应的值
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);
}
接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出,从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
内核向寄存器传递几个参数后,中断调用syscall函数
sys_call:
call save
push dword [p_proc_ready]
sti
push ecx
push ebx
call [sys_call_table + eax * 4]
add esp, 4 * 3
mov [esi + EAXREG - P_STACKBASE], eax
cli
ret
syscall将字符串中的字节“Hello 1170300825 lidaxin”从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
getchar 由宏实现:#define getchar() getc(stdin)。getchar有一个int型的返回值.当程序调用getchar时.程序就等着用户按键.用户输入的字符被存放在键盘缓冲区中.直到用户按回车为止(回车字符也放在缓冲区中).当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符.getchar函数的返回值是用户输入的字符的ASCII码,如出错返回-1
异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用read读取缓冲区内的ascii码,直到接受到回车键才返回。
8.5本章小结
本章主要介绍了Linux的IO设备管理方法、Unix IO接口及其函数,分析了printf函数和getchar函数,加深了对Unix I/O以及异常中断等的了解
结论
hello程序是每一位程序员所必须经过的一步。它非常简单,极简主义的代表。但是它本质上与复杂的程序没有两样。麻雀虽小,五脏俱全,hello从出生到终结每一步都是前面很多的计算机科学家铺好的路,结构十分精巧。作为程序员,这个过程也让我们深入地了解了计算机的底层构造
hello的一生
附件
列出所有的中间产物的文件名,并予以说明起作用。
文件名 | 文件作用 |
---|---|
hello.c | hello源代码 |
hello.i | 预处理文本文件 |
hello.s | 编译后的汇编文件 |
hello.o | 汇编后的可重定位文件 |
hello | 链接后的可执行文件 |
hello.txt | hello.o的反汇编代码 |
helloelf.txt | hello的ELF文本 |
helloobjdump.txt | hello的反汇编代码 |
参考文献
为完成本次大作业你翻阅的书籍与网站等
[1]兰德尔E.布莱恩特 大卫R.奥哈拉伦. 深入理解计算机系统(第3版).机械工业出版社. 2018.4
[2] getchar()函数 - 发展才是硬道理 - 博客园
https://www.cnblogs.com/develop-me/p/5675766.html
[3] UNIX 常用IO函数 - 技术&邂逅 - CSDN博客
https://blog.csdn.net/lingjun_love_tech/article/details/40706599
[4] linux文件操作函数(open、write、read、close)- storyteller的博客- CSDN博客
https://blog.csdn.net/u014650722/article/details/51563679
[5] ELF文件-段和程序头 - 王文清的博客 - CSDN博客
https://blog.csdn.net/u011210147/article/details/54092405
[6] Unix下五种IO模型简介 - 南加无雨 - CSDN博客
https://blog.csdn.net/samuelcoulee/article/details/33740407
[7] 逻辑地址到线性地址的转换 - xuwq2015的博客 - CSDN博客
https://blog.csdn.net/xuwq2015/article/details/48572421
[8] [转]printf 函数实现的深入剖析 - Pianistx - 博客园
https://www.cnblogs.com/pianist/p/3315801.html
[9] 逻辑地址、线性地址和物理地址之间的转换 - 孤独剑 - CSDN博客
https://blog.csdn.net/gdj0001/article/details/80135196
[10] X86在逻辑地址、线性地址、理解虚拟地址和物理地址 - phlsheji - 博客园
https://www.cnblogs.com/bhlsheji/p/4868964.html