哈尔滨工业大学2022春计算机系统大作业

摘  要

    本文主要阐述了一个孤独的hello.c文件是怎么在linux系统中一步步地经过预处理、编译、汇编、链接等过程,在CPU、RAM、Cache、OS等的帮助下,形成了一个庞大的家族,诸如hello.i、hello.s、hello.o、hello.elf、hello等,克服重重困难进行实现,最后又是怎么在shell中完美收场。在这个实现过程中,我们还回顾了shell的存储管理、进程管理、IO管理等相关知识,也了解到了虚拟内存、信号处理等内容。

关键词:预处理;编译;汇编;链接;进程;异常与信号处理;虚拟内存;存储管理;I/O管理                       

目  录

第1章 概述... - 4 -

1.1 Hello简介... - 4 -

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

1.3 中间结果... - 4 -

1.4 本章小结... - 5 -

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

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

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

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

2.4 本章小结... - 7 -

第3章 编译... - 8 -

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

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

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

3.4 本章小结... - 12 -

第4章 汇编... - 13 -

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

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

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

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

4.5 本章小结... - 16 -

第5章 链接... - 17 -

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

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

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

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

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

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

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

5.8 本章小结... - 23 -

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

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

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

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

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

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

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

6.7本章小结... - 27 -

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

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

7.2 Intel逻辑地址到线性地址的变换-段式管理... - 28 -

7.3 Hello的线性地址到物理地址的变换-页式管理... - 28 -

7.4 TLB与四级页表支持下的VA到PA的变换... - 29 -

7.5 三级Cache支持下的物理内存访问... - 30 -

7.6 hello进程fork时的内存映射... - 31 -

7.7 hello进程execve时的内存映射... - 31 -

7.8 缺页故障与缺页中断处理... - 32 -

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

7.10本章小结... - 36 -

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

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

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

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

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

8.5本章小结... - 39 -

结论... - 40 -

附件... - 41 -

参考文献... - 42 -

第1章 概述

1.1 Hello简介

P2P(From Program to Process):

编写完成的hello.c文件,首先经过预处理器预处理生成hello.i文件;再经过编译器编译,生成汇编代码文件hello.s;再经过汇编器翻译成一个重定位目标文件hello.o;最后使用链接器将多个可重定位目标文件组合起来,形成一个可执行目标文件hello。

用户通过shell输入./hello命令开始执行程序,shell通过fork函数创建它的子进程,再由子进程执行execve函数加载hello。

图1.1 hello.c执行流程

020(Zero-0 to Zero-0):

在execve函数执行hello程序后,内核为其映射虚拟内存、分配物理内存;程序开始执行,内核为程序分配时间片执行逻辑控制流。当hello运行结束,由shell回收hello进程,删除有关的数据结构。

1.2 环境与工具

(1)硬件信息:

图1.2 硬件信息

(2)软件环境:Windows 10 64位;Vmware 15.5;Ubuntu 20.04.4

(3)使用工具:Codeblocks;Objdump;Gdb;

1.3 中间结果

hello.c:源代码

hello.i:预处理后的文本文件

hello.s:编译之后的汇编文件

hello.o:汇编之后的可重定位目标执行文件

hello:链接之后的可执行文件

hello0.elf:hello.o的ELF格式

hello.elf:hello的ELF格式

hello0.txt:hello.o反汇编代码

hello.txt:hello的反汇编代码

1.4 本章小结

   本章主要是介绍了hello的执行流程(P2P和020)以及实现过程中的软硬进环境和我们使用的开发与调试工具。

第2章 预处理

2.1 预处理的概念与作用

    预处理的概念:在源程序被编译器处理之前,cpp根据源文件中的宏定义、条件编译等命令对源文件作以修改。

预处理的主要作用:

①将源文件中以”include”格式包含的文件复制到编译的源文件中。

②用实际值替换用“#define”定义的字符串。

③根据“#if”后面的条件决定需要编译的代码。

2.2在Ubuntu下预处理的命令

预处理命令:gcc -E hello.c -o hello.i

图2.1 生成hello.i

2.3 Hello的预处理结果解析

    在hello.i中,依旧有源c程序代码,但是其中大部分内容都是新增加的,比如对宏的展开。

以下是hello.i的部分截图:

图2.2 hello.i节选

2.4 本章小结

    本章介绍了预处理的相关概念、作用和linux系统中的预处理指令,并通过查看hello.i文件和hello.c文件对比他们的不同。

第3章 编译

3.1 编译的概念与作用

编译的概念:把源程序转化为目标程序的操作就叫做编译。此处的编译是指从hello.i 到hello.s 即预处理后的文件转化为汇编语言程序。

编译的作用:将以高级程序设计语言书写的源程序作为输入,经过编译后,输出以汇编语言表示的目标程序。

3.2 在Ubuntu下编译的命令

编译的命令:gcc -S hello.c -o hello.s

图3.1 生成hello.s

3.3 Hello的编译结果解析

hello.c源程序如下图:

图3.2 hello.c源程序

3.3.1 伪指令:

其中.file 声明源文件,.text指示代码段,.section指示rodata段。

3.3.2 数据:

通过查看hello.s文件我们发现,栈指针开辟了32个字节大小的空间,局部变量i保存在-20(%rbp)

3.3.3 赋值:

  使用数据传送指令----使用数据传送指令——MOV类进行赋值,MOV类由movb,movw,movl,movq组成。

3.3.4 算术操作:

(1)加: x=x+y汇编语言是addq y,x

(2)减: x=x-y 汇编语言是subq y,x

(3)乘: x=x*y 汇编语言是imulq y,x

(4)异或:x=x^y 汇编语言是 xor x,y

(5)或:x=x|y 汇编语言是 or x,y

(6)与:x=x&y 汇编语言是 and x,y

3.3.5 数组/指针/结构操作:

(1)数组:取数组头指针加上第i位偏移量来处理。

(2)指针与数组类似,本程序刚开始使用movq -32(%rbp), %ra使得%rax表示*argv,使用addq $8, %rax使得%rax指向argv[1] ,使用movq (%rax), %rax使得%rax取得argv[1]的值,使得movq %rax, %rsi使得%rsi指向argv[1]。

(3)结构体:通过结构体内部的偏移量来访问。

3.3.6 控制转移:

If语句:该程序中使用cmp类比较指令将argc与4比较,如果不等于4,则跳转到L2执行。

For循环:该程序先使用MOV类数据转移指令将局部变量i赋值为0,之后每次操作完成使用算术指令add给i加1,之后使用cmp类比较指令将i与7比较,小于7的话跳转到L4。

3.3.7 函数操作:

该程序先使用被调用者保存,当某个程序调用这些寄存器,被调用寄存器会先保存这些值然后再进行调用,且在调用结束后恢复被调用之前的值。

这里将printf转换成了puts,把.L0段的立即值传入%rdi,然后call跳转到puts。这里的exit是把立即数1传入到%edi中,然后call跳转到exit。

这里的printf有三个参数,第一个是.LC1中的格式化字符串%eax中,后面的两个依次是%rdi,%rsi,然后跳转到printf。这里的sleep有一个参数传到%edi中,之后call跳转到 sleep中。

getchar不需要参数,直接call跳转即可。

3.4 本章小结

本章介绍了编译的概念和他在hello.c实现过程中的作用,重点分析了hello.c中的语句在hello.s文件的表示。

第4章 汇编

4.1 汇编的概念与作用

概念:汇编阶段就是把编译阶段生成的”.s”文件转成可重定位目标文件的过程。

作用:把汇编语言变成可以识别的机器语言,便于后面链接阶段识别。

4.2 在Ubuntu下汇编的命令

指令:gcc -c hello.c -o hello.o

                                                       图4.1 生成hello.o

4.3 可重定位目标elf格式

  使用readelf -a hello.o > hello.elf得到hello.elf文件。

                                                    图4.2 ELF文件结构

  (1)ELF头:

     

                          图4.3 ELF头信息

      其中包含魔数、文件版本、入口虚拟地址、段表文件偏移、节表文件偏移、Elf_Header的大小、段头的大小与起点和数量、节头表的大小与起点和数量、节字符串表的节索引、字的数量、处理器特定的标志等内容。

(2)节头

 

                                                          图4.4 节头信息

(3)重定位节

图4.5 重定位信息

4.4 Hello.o的结果解析

4.4.1转移控制:

  在第三章中,argv存储在-0x20(%rbp)中,而在反汇编得到的结果中他存储在-0x14(%rbp)中,而且hello.s中他会跳转到L2,在反汇编中,他由于经过重定位,会跳转到2f这样一个确定的位置。

将i与7作比较,如果小于,跳转到38的位置。

4.4.2 函数操作:

 

在汇编代码中函数调用时直接使用函数名称,而在反汇编的文件中call之后为main+偏移量,即用具体的地址表示。在.rela.text节中为其添加重定位条目等待链接。

4.5 本章小结

本章介绍了汇编的概念和他在hello.c实现过程中的作用。通过linux命令我们生成hello0.elf、文件hello0.txt文件和hello.o文件,观察这两个文件有助于我们了解ELF文件中所包含的文件信息和.o文件反汇编之后与.s文件的差别。

第5章 链接

5.1 链接的概念与作用

概念:通过符号将多个模块拼接为一个独立的程序的过程就叫做链接。执行的时机有:(1)编译时:源代码被翻译成机器码。静态库,共享库(动态库),可重定位目标文件;(2)加载时:程序被加载器加载到内存并执行时。静态库,共享库(动态库),可重定位目标文件;(3)运行时:由应用程序来执行链接。共享库(动态库)/共享目标文件。

作用:链接过程将多个可重定位目标文件合并以生成可执行目标文件。

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.1使用ld命令生成hello

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

使用readelf -a hello > hello.elf命令得到hello.elf文件。

(1)ELF头:

                       图5.2 ELF头信息

与hello0.elf对比来看,魔数没有变化,但是程序入口地址发生了改变,由0X0变成了0X4010f0,段头和节头表开始地址、大小改变,节头数量增多,字符串表索引节头位置变化。

(2)节头:

                       图5.3 节头表信息

(3)段头:

                            图5.4 段头信息

5.4 hello的虚拟地址空间

  查看edb中的data dump可以得到hello的虚拟空间地址,从0x401000开始载入程序,一直到0x402000,对应查看段头可以发现,0x401000正好是程序载入地址,查看节头表可以发现,0x401000正好是.init节载入的地址。

图5.5 虚拟地址空间信息

5.5 链接的重定位过程分析

在hello.o反汇编得到的文件中只有.text一个节,同时,main函数的地址从0x0开始,而在hello得到的反汇编文件中多了很多节,同时他的数据有了明确的地址,而不是从0x0开始。

图5.6 init节信息

图5.7 .plt节信息

图5.8 .plt.sec节信息

图5.9 _start信息

图5.10 main信息

5.6 hello的执行流程

_start :0x00000000004010f0

__libc_csu_init :0x00000000004011c0

_init :0x0000000000401000

main :0x0000000000401125

puts@plt :0x0000000000401090

exit@plt :0x00000000004010d0

_fini :0x0000000000401238

5.7 Hello的动态链接分析

  

分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。

在.got节中存放的是变量的全局偏移量,在链接之后由于动态添加了很多目标执行所需要的程序,所以.got节中的内容会发生改变。

我们打开ELF文件,找到节头表,找到.got节的首地址,是0x403ff0,在执行链接之前,如图5.11所示,链接后如图5.12所示。

图5.11 连接前的.got节

图5.12 连接后的.got节

5.8 本章小结

本章主要对hello.c的链接和ELF文件格式做了详细介绍,同时使用edb和obdjump使我们对连接过程中虚拟空间的使用和动态链接过程有了更直观的了解。

第6章 hello进程管理

6.1 进程的概念与作用

概念:进程是一个执行中程序的实例。进程是具有独立功能的一个程序关于某个数据集合的一次运行活动,因而进程具有动态含义。进程是os对spu执行的程序的运行过程的一种抽象。

作用:进程是一个执行中程序的实例。所有程序都运行在某个进程的上下文中,操作系统通过管理进程来实现对程序的正确执行。

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

作用:shell是运行在终端中的文本互动程序,shell将我们的操作或命令解释为计算机可识别的二进制命令,传递给内核,以便调用计算机硬件执行相关的操作;计算机执行完命令后,再通过shell解释为成自然语言。

处理流程:

(1)从终端读入输入的命令。

(2)将输入字符串分离获得所有的参数

(3)如果是内置命令则立即执行

(4)否则调用相应的程序为其分配子进程并运行

(5)接受键盘输入信号,并对这些信号进行相应处理

6.3 Hello的fork进程创建过程

父进程通过调用fork函数创建一个新的运行的子进程,新创建的子进程几乎但不完全与父进程相同:子进程得到与父进程虚拟地址空间相同的(但是独立的)

一份副本;子进程获得与父进程任何打开文件描述符相同的副本;最大区别:子进程有不同于父进程的PID。

fork函数:被调用一次,返回两次。

6.4 Hello的execve过程

该函数声明为:int execve(char *filename, char *argv[], char *envp[])。

它在当前进程中载入并运行一个新程序,其中filename:可执行文件目标文件或脚本(用#!指明解释器,如 #!/bin/bash),argv:参数列表,惯例:argv[0]==filename,envp:环境变量列表:"name=value" strings (e.g., USER=droh),getenv, putenv, printenv

它会覆盖当前进程的代码、数据、栈,他与被覆盖进程有相同的PID,继承已打开的文件描述符和信号上下文并且调用一次并从不返回,除非有错误。

图6.1 用户栈的组织结构

6.5 Hello的进程执行

系统中每个程序都运行在某个进程的上下文中。上下文是程序正确运行所需要的状态,由内核进行维持。

一个运行多个进程的系统,进程逻辑流的执行可能是交错的。每个进程执行它的流的一部分,然后被抢占,然后轮到其他进程。如果两个逻辑流在时间上有重叠,则称这两个进程是并发的(并发进程),否则他们是顺序的。并发进程的控制流物理上是不重叠的,然而,我们可以认为并发进程是并行运行的。一个进程执行它的控制流的一部分时间叫做时间片。

图6.2 进程的切换

在hello中,程序执行了sleep系统调用,引发了陷阱异常。此时会从用户模式进入内核模式,使程序休眠一段时间,将控制转给其他进程。当sleep结束后,发送信号给内核,进入内核状态处理异常,此时hello程序得以重新回到用户模式。当执行getchar函数时,会使用read系统调用,再次产生上下文切换。

6.6 hello的异常与信号处理

(1)异常种类:

图6.3 异常种类

(2)常见信号:

图6.4 常见信号

(3)shell指令:

图6.5 ps查看

6.7本章小结

  本章介绍了进程的相关概念和shell的使用,展示了hello在shell中的详细执行过程以及遇到信号时的处理。

第7章 hello的存储管理

7.1 hello的存储器地址空间

(1)逻辑地址:指由程序产生的段内偏移地址。

(2)线性地址:指虚拟地址到物理地址变换的中间层,是处理器可寻址的内存空间(称为线性地址空间)中的地址。程序代码产生的逻辑地址加上相应段基址就成了一个线性地址。如果启用了分页机制,那么线性地址可以再经过变换产生物理地址。若是没有采用分页机制,那么线性地址就是物理地址。

(3)虚拟地址:是由程序产生的由段选择符和段内偏移地址组成的地址。

(4)物理地址:指内存中物理单元的集合。

7.2 Intel逻辑地址到线性地址的变换-段式管理

逻辑地址=段选择符+偏移量

每个段选择符大小为16位,段描述符为8字节。每个段的首地址都存放在自己的段描述符中,而所有的段描述符都存放在一个描述符表中。而要想找到某个段的描述符必须通过段选择符才能找到。

通过段选择符我们就可以找到我们想要的段描述符,从而获取某个段的首地址,然后再将从段描述符中获取到的首地址与逻辑地址的偏移量相加就得到了线性地址。

7.3 Hello的线性地址到物理地址的变换-页式管理

分页机制把线性地址空间和物理地址空间分别划分为大小相同的块。这样的块称之为页。通过在线性地址空间的页与物理地址空间的页之间建立的映射,分页机制实现线性地址到物理地址的转换。

图7.1线性地址到物理地址的变换

虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。利用页表来管理虚拟页,页表就是一个页表条目(PTE)的数组,每个PTE由一个有效位和一个n位地址字段组成。

Cpu首先会生成一个虚拟地址,传递给MMU,MMU利用VPN获取PPN与保持和VPO不变的PPO组成物理地址。

7.4 TLB与四级页表支持下的VA到PA的变换

图7.2四级页表的地址翻译

图7.3 1-3级页表条目格式

图7.4 4级页表条目格式

每个条目引用一个 4KB子页表:

P: 子页表在物理内存中 (1)不在 (0).

R/W: 对于所有可访问页,只读或者读写访问权限.

U/S: 对于所有可访问页,用户或超级用户 (内核)模式访问权限.

WT: 子页表的直写或写回缓存策略.

A: 引用位 (由MMU 在读或写时设置,由软件清除).

PS: 页大小为4 KB 或 4 MB (只对第一层PTE定义).

D:修改位,告知是否写回

Page table physical base address: 子页表的物理基地址的最高40位 (强制页表

4KB 对齐)

XD: 能/不能从这个PTE可访问的所有页中取指令.

虚拟地址的VPN被划分为VPN1,VPN2,VPN3,VPN4。CR3寄存器中有L1页表的地址,根据VPN1能够在L1页表找到相应PTE,得到L2页表的基地址,一次类推,最终我们得到物理地址的PPN。

7.5 三级Cache支持下的物理内存访问

图7.5 三级Cache支持下的物理内存访问

  物理地址PA被分成3块,CT(标记)、CI(索引)、CO(偏移),之后在L1中寻找,若命中,则返回对应块偏移的数据。否则,L1不命中,我们需要前往L2,L3甚至是主存中得到对应的数据。

7.6 hello进程fork时的内存映射

Fork函数为新进程创建虚拟内存,他会创建当前进程的的mm_struct, vm_area_struct和页表的原样副本。将两个进程中的每个页面都标记为只读,两个进程中的每个区域结构(vm_area_struct)都标记为私有的写时复制(COW),在新进程中返回时,新进程拥有与调用fork进程相同的虚拟内存,随后的写操作通过写时复制机制创建新页面。

7.7 hello进程execve时的内存映射

图7.6 execve时的内存映射

它首先删除已存在的用户区域,之后创建新的区域结构,其中的代码和初始化数据映射到.text和.data区(目标文件提供),.bss和栈映射到匿名文件。hello程序与共享对象链接,这些对象动态链接到hello程序,然后映射到用户虚拟地址空间。再设置PC,指向代码区域的入口点。

7.8 缺页故障与缺页中断处理

有三种情况:

(1)段错误: 访问一个不存在的页面,此时会中断程序执行。

(2)正常缺页,会选择牺牲页,换入新的页面并进行页表更新,完成缺页处理。

(3)保护异常: 例如,违反许可,写一个只读的 页面(Linux 报告 Segmentation fault)

7.9动态存储分配管理

Printf会调用malloc,请简述动态内存管理的基本方法与策略。

7.9.1隐式空闲列表

(1)记录空闲块的方法:通过头部中的大小字 段—隐含地连接所有块。

图7.7隐式空闲链表组织堆

(2)找到一个空闲块:

①首次适配:从头开始搜索空闲链表,选择第一个 合适的空闲块,此时,搜索时间与总块数 ( 包括已分配和空闲块 ) 成线性关系,在靠近链表起始处留下小空闲块的 “碎片”。

②下一次适配 (Next fit):和首次适配相似,只是从链表中上一次查询结束的地方开始,比首次适应更快: 避免重复扫描那些无用块。

③最佳适配 (Best fit): 查询链表,选择一个最好的空闲块:适配,剩余最少空闲空间,保证碎片最小——提高内存利用率,通常运行速度会慢于首次适配。

(3)分配空闲块:分配块比空闲块小, 我们可以把空闲块分割成两部分。

(4)释放一个块:清除 “已分配 (allocated) ” 标志。

(5)合并:合并相邻的空闲块。

边界标记法:在空闲块的“底部”标记 大小/已分配。允许我们反查 “链表” ,但这需要额外的空间。

图7.8使用边界标记 的堆块格式

图7.9 合并情况1

图7.10合并情况2

图7.11 合并情况3

图7.12 合并情况4

边界标记法缺陷:显著的额外内存开销。

7.9.2显式空闲链表

(1)记录空闲块的方法:在空闲块中使用指针。

图7.13 显式空闲链表下的堆块格式

只记录空闲块 链表, 而不是所有块,“下一个” 空闲块可以在任何地方,因此我们需要存储前驱/后继指针,而不仅仅是大小,还需要合并边界标记。在物理上,块的顺序是任何的。

(2)分配:

图7.14显式空闲链表下的堆块分配前后

(3)释放:

①LIFO (last-in-first-out)后进先出法:将新释放的块放置在链表的开始处。

②地址顺序法:按照地址顺序维护链表: addr(祖先) < addr(当前回收块) < addr(后继)。

7.9.3分离的空闲链表

(1)记录空闲块的方法:按照大小分类,构成不同大小的空闲链表。

每个大小类中的块构成一个空闲链表,通常每个小块都有单独的大小类,对于大块: 按照2的幂分类。

(2)当分配器需要一个大小n的块时:搜索相应的空闲链表,使其满足m > n,如果找到了合适的块: 拆分块,并将剩余部分插入到适当的可选列表中;如果找不到合适的块, 就搜索下一个更大的大小类的空闲链表直到找到为止。

如果空闲链表中没有合适的块:向操作系统请求额外的堆内存 (使用sbrk()),从这个新的堆内存中分配出n字节将剩余部分放置在适当的大小类中。

(3)释放:合并,并将结果放置到相应的空闲链表中。

7.10本章小结

本章介绍了系统对于hello的存储管理,辨析了逻辑地址、线性地址、物理地址、虚拟地址的区别和转化。如intel段式管理、页式管理,介绍了程序的虚拟地址是怎么样翻译为物理地址,如分级页表的情况和三级cache的情况。同时分析程序运行过程中fork,execve函数进行的内存映射,还介绍了缺页异常的处理及动态存储的分配。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

以下格式自行编排,编辑时删除

设备的模型化:文件

设备管理:unix io接口

一个 Linux 文件 就是一个 m 字节的序列:B0 , B1 , .... , Bk, .... , Bm-1。所有的I/O设备都被模型化为文件:/dev/sda2(用户磁盘分区),/dev/tty2(终端)。I/O操作可看作对相应文件的读或写。

8.2 简述Unix IO接口及其函数

Linux内核给出的一个简单、低级的应用接口,能够以统一且一致的方式执行 I/O操作,包括:

(1)打开和关闭文件:open()and close()

打开文件:调用open函数,例:fd = open("/etc/hosts", O_RDONLY)) < 0。

返回一个小的描述符数字——文件描述符。若 fd == -1 说明发生错误。Linux内核创建的每个进程都有三个打开的文件: 0: 标准输入 (stdin) ;1: 标准输出 (stdout); 2: 标准错误 (stderr)。

关闭文件:调用close函数,例:((retval = close(fd)) < 0。关闭一个已经关闭的文件会出错。

(2)读写文件:read() and write()

读文件,调用read函数,例:(nbytes = read(fd, buf, sizeof(buf)))。返回值表示的是实际传送的字节数量,返回类型 ssize_t 是有符号整数,nbytes < 0 表示发生错误,不足值 (nbytes < sizeof(buf) ) 是可能的,不是错误!

写文件,调用write函数,例:(nbytes = write(fd, buf, sizeof(buf))。返回值表示的是从内存向文件fd实际传送的字节数量,nbytes < 0 表明发生错误,同读文件一样, 不足值是可能的,并不是错误!

出现“不足值”的几种情况:读时遇到EOF:读取位置k≥文件大小m;从终端读文本行(返回文本行的大小);读写网络套接字(网络延迟导致返回不足值)。

以下几种情况不会出现“不足值”:读磁盘文件 (除了 EOF);写磁盘文件。

为了避免不足值问题,就需要通过反复调用 read和write处理不足值。

(3)改变当前的文件位置,指示文件要读写位置的偏移量:lseek()

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;
    }

(va_list)((char*)(&fmt) + 4)代表的是printf参数中的第一个参数,printf的参数数量不确定。

vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出,返回的是要打印出来的字符串的长度。

write:
     mov eax, _NR_write
     mov ebx, [esp + 4]
     mov ecx, [esp + 8]
     int INT_VECTOR_SYS_CALL

     这里是给几个寄存器传递了几个参数,然后一个int结束,这样的int表示要调用中断门了。通过中断门,来实现特定的系统服务。一个int INT_VECTOR_SYS_CALL表示要通过系统来调用sys_call这个函数。

INT_VECTOR_SYS_CALL的实现:
  init_idt_desc(INT_VECTOR_SYS_CALL, DA_386IGate, sys_call, PRIVILEGE_USER);

sys_call的实现:
     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

字符显示驱动子程序:从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函数的函数原型为:int getchar(void), getchar其实返回的是字符的ASCII码值(整数)。getchar在读取结束或者失败的时候,会返回EOF。

当我们使用键盘输入字符时,字符接受按键扫描码转成ascii码,保存到系统的键盘缓冲区,getchar会从缓冲区提取字符而不是直接从键盘输入。getchar调用read函数,将缓冲区读入到buf中,并将长度送给n,再重新令bb指针指向buf。最后返回buf中的第一个字符(如果长度n < 0,则报EOF错误)。getchar直到接受到回车键才返回一次,且只返回第一个字符作为getchar函数的值,如果有循环或足够多的getchar语句,就会依次读出缓冲区内的所有字符直到’\n’。

8.5本章小结

本章介绍了Linux的IO设备管理方法和Unix IO接口及其函数,并且详细论述了printf函数和getchar函数的执行流程,包括系统调用和IO管理。

结论

用计算机系统的语言,逐条总结hello所经历的过程。

你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。

hello.c终于变成了hello并顺利执行,这期间,他经历诸多:

(1)首先hello.c文件经过预处理,生成hello.i文件。

(2)之后,hello.i文件经过编译,生成hello.s汇编文件。

(3)然后,hello.s文件经过汇编生成可重定位目标文件hello.o

(4)最后,hello.o与其他所需要的库函数经过动态链接生成可执行目标文件hello

(5)运行hello,首先bash shell 调用fork函数生成子进程,调用execve函数载入hello,为其分配虚拟内存,并在函数执行到入口时载入物理内存。

(6)之后,hello通过进程管理,单独的享有某段时间的CPU,执行自己的逻辑控制流。在此期间,CPU发出虚拟地址,由MMU通过TLB和页表将虚拟地址映射成为物理地址,根据物理地址通过cache和进行内存访问提取信息。在运行时,还会遇到信号并接受他,调用信号处理程序加以处理。

(7)最后,程序执行结束,shell父进程会回收fork出来的子进程,内核会删去该过程中创建的一切数据结构。

通过对hello一生的探索与总结,我对程序从无到有的执行过程有了更深刻的认识,尤其是程序的动态链接、程序执行时shell的操作、fork和execve函数在其中发挥的作用、虚拟内存在其中的作用、在执行过程中malloc申请动态内存时堆的管理、执行过程中应对键盘输入等的IO管理以及信号的处理,其中有很多困惑与不解,一开始并不理解虚拟内存和信号管理,但随着一步步探索,终于明白了很多,伴随着我对csapp这门课的逐步理解,hello也终于走完了它的一生。

附件

hello.c:源代码

hello.i:预处理后的文本文件

hello.s:编译之后的汇编文件

hello.o:汇编之后的可重定位目标执行文件

hello:链接之后的可执行文件

hello0.elf:hello.o的ELF格式

hello.elf:hello的ELF格式

hello0.txt:hello.o反汇编代码

hello.txt:hello的反汇编代码

参考文献

[1]  林来兴. 空间控制技术[M]. 北京:中国宇航出版社,1992:25-42.

[2]  辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.

[3]  赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).

[4]  谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.

[5]  KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.

[6]  CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.

[7] Randal E.Bryant David R.O’Hallaron. 深入理解计算机系统(第三版). 机械工业出版社,2016.

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值