HITICS2022春大作业

 

计算机系统

大作业

题     目  程序人生-Hello’s P2P 

专       业          计算机       

学     号        120L020131      

班     级         03003         

学       生         黄晨        

指 导 教 师                       

计算机科学与技术学院

2022年5月

摘  要

几乎每个菜鸟程序员都编写过hello程序。或是用c语言,或是java,或是python等等。然而有没有想过,hello是如何从你编写的代码变成计算机能够实现的程序?是如何在Linux系统上运行的?

程序员在分析hello程序的一生后,方能理解Linux系统中的奥妙。

关键词:C语言;Linux;编译;进程;                         

目  录

第1章 概述............................................................................................................. - 4 -

1.1 Hello简介...................................................................................................... - 4 -

1.2 环境与工具..................................................................................................... - 4 -

1.3 中间结果......................................................................................................... - 4 -

1.4 本章小结......................................................................................................... - 4 -

第2章 预处理......................................................................................................... - 5 -

2.1 预处理的概念与作用..................................................................................... - 5 -

2.2在Ubuntu下预处理的命令.......................................................................... - 5 -

2.3 Hello的预处理结果解析.............................................................................. - 5 -

2.4 本章小结......................................................................................................... - 5 -

第3章 编译............................................................................................................. - 6 -

3.1 编译的概念与作用......................................................................................... - 6 -

3.2 在Ubuntu下编译的命令.............................................................................. - 6 -

3.3 Hello的编译结果解析.................................................................................. - 6 -

3.4 本章小结......................................................................................................... - 6 -

第4章 汇编............................................................................................................. - 7 -

4.1 汇编的概念与作用......................................................................................... - 7 -

4.2 在Ubuntu下汇编的命令.............................................................................. - 7 -

4.3 可重定位目标elf格式.................................................................................. - 7 -

4.4 Hello.o的结果解析....................................................................................... - 7 -

4.5 本章小结......................................................................................................... - 7 -

第5章 链接............................................................................................................. - 8 -

5.1 链接的概念与作用......................................................................................... - 8 -

5.2 在Ubuntu下链接的命令.............................................................................. - 8 -

5.3 可执行目标文件hello的格式..................................................................... - 8 -

5.4 hello的虚拟地址空间................................................................................... - 8 -

5.5 链接的重定位过程分析................................................................................. - 8 -

5.6 hello的执行流程........................................................................................... - 8 -

5.7 Hello的动态链接分析.................................................................................. - 8 -

5.8 本章小结......................................................................................................... - 9 -

第6章 hello进程管理.................................................................................... - 10 -

6.1 进程的概念与作用....................................................................................... - 10 -

6.2 简述壳Shell-bash的作用与处理流程...................................................... - 10 -

6.3 Hello的fork进程创建过程...................................................................... - 10 -

6.4 Hello的execve过程.................................................................................. - 10 -

6.5 Hello的进程执行........................................................................................ - 10 -

6.6 hello的异常与信号处理............................................................................. - 10 -

6.7本章小结........................................................................................................ - 10 -

第7章 hello的存储管理................................................................................ - 11 -

7.1 hello的存储器地址空间............................................................................. - 11 -

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 -

7.9动态存储分配管理........................................................................................ - 11 -

7.10本章小结...................................................................................................... - 12 -

第8章 hello的IO管理.................................................................................. - 13 -

8.1 Linux的IO设备管理方法........................................................................... - 13 -

8.2 简述Unix IO接口及其函数........................................................................ - 13 -

8.3 printf的实现分析........................................................................................ - 13 -

8.4 getchar的实现分析.................................................................................... - 13 -

8.5本章小结........................................................................................................ - 13 -

结论......................................................................................................................... - 14 -

附件......................................................................................................................... - 15 -

参考文献................................................................................................................. - 16 -

第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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值