CSAPP大作业-hello的一生

CSAPP大作业-hello的一生

摘 要

本文通过介绍一个简单程序Hello的一生,详细分析了一个程序由诞生到执行再到消亡的典型过程。虽然程序执行的过程在程序员眼中只是屏幕上的显示的字符串,但在短短几ms内,程序却经历了预处理,编译,汇编链接,进程管理,IO管理,内存分配与回收等等一系列复杂的流程。同时也在本文中梳理了书本的知识,由hello的一生将整本书的内容连贯起来。

关键词:程序人生;计算机系统;程序分析;预处理;编译;汇编;链接;进程

第1章 概述

1.1 Hello简介

程序hello由键盘键入,经历P2P的过程后由文本文件hello.c一步步变成可执行文件hello,再由shell经历了O2O过程:分配内存,执行,再到消亡,被回收,由此完成一个程序复杂又奇妙的一生。

P2P:From Program to Process

指的是程序由一个项目变成一个进程的过程,程序键入键盘并保存为一个名为hello.c的program,之后gcc编译器读取该program,经历预处理得到hello.i文件,再由编译器将文本文件hello.i翻译成汇编程序hello.s,之后通过汇编器将hello.s翻译成机器语言,生成可重定位文件hello.o,最后一步由链接器得到可执行文件hello。生成可执行文件后,program:hello即准备就绪,通过shell安排上岗,为其准备执行的进程process

O2O:From OS to IO

指的是可执行程序经过系统os,shell通过execve为其fork子进程加载hello,为hello创建工作场所:代码段,数据段,bss,栈区等等,通过映射共享预取和设置程序计数器,进入main函数。CPU控制其上岗时间,执行逻辑控制流,UnixI/O管理hello的工作,使其产生屏幕上的输出。最后shell将退休的hello回收,OS中的hello将被删除,至此为止hello完成了它的一生

1.2 环境与工具

1.2.1 硬件环境:

处理器:Corei7-1065G7
CPU 1.30GHz 1.50GHz;16GB;1T disk

1.2.2 软件环境:

Windows 10 64位;Ubuntu18.04 LTS 64位;codeblocks

1.2.3 开发工具

Codeblocks,gdb,odjdump,edb,readelf等

1.3 中间结果

Hello.c

Hello的c语言代码

Hello.i

预处理之后的文本文件

Hello.s

编译之后产生的汇编文件

Hello.ld

链接后的文件

Hello.o

可重定位的目标文件

Hello

可执行文件hello

Helloo.objdump

Hello.o反汇编文件

Hello.elf

Hello的ELF格式

Hello.objdump

Hello的反汇编文件

1.4 本章小结

对hello从诞生到执行到消亡的P2P和O2O过程进行了简介,梳理全文脉络,同时介绍了整个过程中所使用的环境和工具。

第2章 预处理

2.1 预处理的概念与作用

预处理的概念:是c语言的一个重要功能,由预处理程序负责完成,将源文件.c预处理成.i文件,主要处理#开始的预编译指令

作用:合理使用预处理功能编写的程序便于阅读,修改,调试,有利于模块化设计

主要功能:1.将源文件中include包含的文件复制到源文件中,例如#include高速预处理器将文件stdio.h加入到源文件中

2.用实际参数值替换宏#define值定义的字符串

3.根据#if,#ifdef等条件决定需要编译的代码

4.删除所有注释/**/ //

5.添加行号和文件标识

6.保留#pragma编译器指令

CSAPP大作业-hello的一生_第1张图片

2.2在Ubuntu下预处理的命令

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

CSAPP大作业-hello的一生_第2张图片

图2.2 linux下c语言预处理指令

2.3 Hello的预处理结果解析

经过预处理程序处理hello.c源文件由23行扩展成为3000多行,main函数被放在了最后,而前面3000多行是hello.c引用的头文件,如stdio.h。

预处理过程中预处理器(cpp)识别到#include这种指令就会在环境中搜寻该头文件并将其递归展开。

下面两图对比可以看出,hello.c中开头的注释被删除,#include包含的几个文件被插入到hello.i中

#include,#include‘stdio.h’这两种表示形式有区别,第一种系统会直接在c库函数头文件所在的目录中查找要包含的文件,第二种是用户在当前目录中查找,如果找不到才回到c库函数头文件所在目录中华查找要包含的文件,一般用于自己编写的头文件

CSAPP大作业-hello的一生_第3张图片
图2.3.1 hello.c源文件

CSAPP大作业-hello的一生_第4张图片
图2.3.2 预处理后的hello.i文件

2.4 本章小结

在预处理过程中hello.c经过cpp的一系列处理变成了hello.i文件,此时hello仍然是一个文本文件,想要被执行还需要继续在编译器等帮助下继续成长变化

第3章 编译

3.1 编译的概念与作用

编译概念:将预处理完的文件进行一系列语法分析及优化后生成相应的汇编文件

作用:将高级语言的文件翻译成机器更好理解的汇编语言文本

主要包括三个步骤:

  1.  词法分析:将字符串转化为内部的表示机构
    
  2.  语法分析:将词法分析得到的标记流生成语法树
    
  3.  生成目标代码:将语法树转化为目标代码
    

注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序

3.2 在Ubuntu下编译的命令

命令:gcc -S hello.i -o hello.s

3.2 在Ubuntu下编译的命令

命令:gcc -S hello.i -o hello.s

图3.2 编译命令以及生成文件hello.s

3.3 Hello的编译结果解析
CSAPP大作业-hello的一生_第5张图片
3.3.1 文件声明解析

CSAPP大作业-hello的一生_第6张图片

1.3.2 数据

Hello.s中主要数据类型有:全局变量,局部变量,指针数组,字符串

  1.  字符串:
    

CSAPP大作业-hello的一生_第7张图片

图3.3.2.1 LC0和LC1段中的字符串

如图,前一个字符串对应源文件中的“Usage:Hello 学号 姓名! 、n”,

该字符串被编码为UTF-8格式

后一个字符串为“Hello %s
%s\n”

均存放在.rodata只读数据段中

  1.  整型变量
    

1)全局变量int sleepsecs

赋值sleepsecs =
2.5

在这里插入图片描述

图 3.2.2.2 变量sleepsecs

在这里插入图片描述

图3.2.2.3 变量sleepsecs的赋值

赋值的时候发生了类型转换,由于2.5是float类型,sleepsecs是int类型,故此处发生了一个隐式类型转换,编译器将2.5舍入为2给sleepsecs赋值。

2)局部变量 int i

局部变量存储在寄存器或者栈空间中,此处i存放在栈空间-4(%rbp)中

在这里插入图片描述
图3.2.2.4 变量i=0的赋值

  1.  算数运算:
    

1)i++:此句意味i=i+1,自增运算符直接利用add指令对该变量保存的值进行加一运算

在这里插入图片描述
图3.2.2.5 i++的汇编代码

2)加载有效地址:
在这里插入图片描述
计算LC1的段地址%rip+.LC1,传递给%rdi

  1.  条件判断并控制转移
    

使用条件判断和标志位实现循环for和条件判断if

1)判断i<10:

将i的值与立即数9进行比较(cmpl),若小于等于(jle)则跳转到L4

在这里插入图片描述
图3.2.2.6 i<10的汇编代码

2)判断argc!=3:将3与该值进行比较,若相等(je)则跳转至L2

在这里插入图片描述

图3.2.2.7 argc!=3的判断

  1.  数组操作
    

字符指针数组 char *argv[]:存储用户输入的命令行信息地址

该数组中每个元素大小为8bit,argv既是数组名也是数组的首地址

图中分别为argv[1],argv[2]的实现

CSAPP大作业-hello的一生_第8张图片

图3.2.2.8 命令行参数数组

  1.  函数操作:
    

1)printf:

printf(“Usage: Hello 学号
姓名!\n”);与该段对应:

printf将%rdi设置为字符串的首地址

CSAPP大作业-hello的一生_第9张图片

图3.2.2.9 printf的实现1

%rdi为打印字符串的首地址,%rsi为argv[1],%rdx为argv[2]

printf(“Hello %s %s\n”,argv[1],argv[2]);与该段对应

图3.2.2.10 printf的实现2

2)sleep

sleep(sleepsecs);将%edi设置为参数sleepsecs传递

在这里插入图片描述

图3.2.2.11 函数sleep的实现

3)exit函数:exit(1);将%edi设置为1

在这里插入图片描述

图3.2.2.12 函数exit的实现

4)getchar函数:

在这里插入图片描述

图3.2.2.13 函数getchar的实现

5)main函数:系统启动函数_libc_start_main_调用,call将下一条指令压栈并跳转至main函数

外部调用向main函数传递参数argc和argv,使用%rdi和%rsi存储,函数结束return 0 即为将%eax设置为0

在这里插入图片描述
图3.2.2.14 return0的实现

%rbp为栈帧的底部,函数在%rbp上分配空间

Leave指令:相当于mov %rbp,%rsp,pop %rbp,恢复栈空间为调用main函数之前的状态

/此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析。/

3.4 本章小结

本节对应于书上与汇编语言相关的章节,总结并分析了编译器是如何处理c语言的各个数据类型和各类操作,如算术操作,关系操作和函数调用的。经过该步骤hello.s已经是更加接近机器层面的汇编代码,离hello“上岗工作“又进了一步。

第4章 汇编

4.1 汇编的概念与作用

汇编的概念:汇编器(as)将hello.s翻译成机器能读懂的机器语言指令,并将这些指令打包成可重定位目标程序hello.o,hello.o是一个二进制文件

作用:产生机器能读懂的代码,使得程序能被机器执行。由于几乎每一条汇编指令都对应于一条机器代码,汇编过程比编译过程更简单。

注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。

4.2 在Ubuntu下汇编的命令

gcc -c helo.s -o hello.o

CSAPP大作业-hello的一生_第10张图片

图4.2 终端生成hello.o指令

4.3 可重定位目标elf格式

Hello.o的elf格式为可重定位目标文件的格式,包括ELF头,段和节头表。

ELF头

CSAPP大作业-hello的一生_第11张图片

图4.3.1 ELF头

ELF头:包含信息为文件结构的说明信息:16字节的标识信息,文件类型,机器类型,节头表偏移,节头表的表项大小,表项个数,生成该文件的系统字大小和字节顺序

节头部表

CSAPP大作业-hello的一生_第12张图片

图4.3.2 节头部表

节头部表描述不同节的位置和大小,目标文件中1每个节都有一个固定大小的条目,相关信息包括节的名称,类型,地址,偏移量,对齐,旗标等

从节头部表中可以得知hello.o共有13个节:

CSAPP大作业-hello的一生_第13张图片

重定位表:

Rela.text和.rela.eh_frame中包含.text节中需要进行重定位的信息,在链接时需要修改这些信息的位置。

两种最基本的重定位类型:

R_X86_64_PC32:重定位一个使用32bitPC相对地址的引用

R_X86_64_32:重定位一个使用32bitPC绝对地址的引用

重定位表的作用:

由于汇编器在生成目标模块时并不知道数据和代码段将存放在内存中的什么位置和模块引用外部定义的函数或者全局变量的位置,所以当汇编器对最终位置未知的目标引用就会生成一个重定位条目让链接器在链接生成可执行文件时应该如何修改这个引用

ELF重定位条目

在这里插入图片描述

图4.3.3 重定位节

这8条重定位信息分别对应于L0,puts函数,exit函数,L1,printf函数,sleepsecs,sleep函数,getchar函数,执行链接时链接器会将这些信息根据重定位条目进行重定位

分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析

4.4 Hello.o的结果解析

objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。

使用指令objdump -d -r hello.o

helloo.objdump 得到helloo.objdump文件

Hello.s

Helloo.objdump

机器语言的构成:由二进制(01序列)的机器指令集合构成,机器指令的构成则是操作码和操作数,汇编语言是最接近于机器语言的底层语言,与机器语言一样由简单指令和操作数构成,几乎一条汇编指令就对应一条反汇编得到的机器语言。(对应唯一的01序列)

二者的差别:

  1.  操作数:hello.s中操作数为十进制,hello.o反汇编代码中操作数为16进制。
    
  2.  分支跳转:反汇编代码中可以看出相对偏移地址取代了hello.s中的标志位
    
  3.  函数调用过程:反汇编代码中相对于main函数的相对偏移地址取代了hello.s中的函数名称,原因在于函数在链接后才能确定运行的地址,在前文中提到的.rela.text节中的重定位条目中汇编器告诉了链接器位置信息。
    
  4.  全局变量的访问:hello.s文件中对于全局变量的访问为LC0和sleepsecs(%rip),而在反汇编代码中是$0x0和0(%rip),原因与函数调用一样,全局变量的地址也是在运行时才确定,访问也需要经过重定位
    
  5.  汇编器在汇编hello.s文件时为每条语句都加上了具体的地址
    
  6.  反汇编得到的代码在每行最前面加上了时钟周期信息
    

说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。*/

4.5 本章小结

在汇编过程中,hello.s被汇编器变为hello.o文件,此时hello.o已经是可以被机器读懂的二进制文件了。Hello.o可重定位目标文件也为后面进行链接做好了准备。此时的hello仍然不能“上岗工作”,还需要进行最后一步链接才能变为可以被系统执行的可执行文件。同时通过反汇编hello.o,了解了汇编代码和机器代码之间的区别和联系。

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

第5章 链接

5.1 链接的概念与作用

概念:链接是指将各种代码和数据片段收集并组合为一个单一文件的过程,这个文件可被加载到内存并执行

作用:比如我们在hello.c中调用了函数printf,该函数存在于一个名为printf.o的单独预编译好的目标文件中,为了生成可执行文件hello,我们必须通过链接器ld将这个文件整合到hello.o中。得到可执行文件才能在内存中加载执行

当我们需要修改一个大型的应用程序时,链接使得我们可以单独修改其中某一个部分再与其他部分重新整合,否则我们每次修改都要重新编译整个文件,极大加大了开销。

注意:这儿的链接是指从 hello.o 到hello生成过程。

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

CSAPP大作业-hello的一生_第14张图片

图5.2 链接过程

使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件

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

可执行目标文件hello的格式与可重定位目标文件的格式相似,由ELF头,节头,段头部表,重定位节等构成

CSAPP大作业-hello的一生_第15张图片

图5.3.1 Hello的ELF头

图5.3.2 hello的节头

该图中包含各段的起始位置,大小等信息

图5.3.3 hello的段节

CSAPP大作业-hello的一生_第16张图片

图 5.3.4 hello的重定位节

分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。

5.4 hello的虚拟地址空间

![在这里插入图片描述](https://img-blog.csdnimg.cn/20191228194940302.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80Mzc4NDUzNw==,size_16,color_FFFFFF,t_70)

图5.4.1 datadump窗口

Datadump窗口中可以看到hello程序中的虚拟地址空间信息

CSAPP大作业-hello的一生_第17张图片
图5.4.2 程序的程序头表

该表在程序执行的时候被使用,作用是告诉链接器运行时应该加载的内容并提供动态链接的信息,提供了各段在虚拟地址空间和物理地址空间的大小,位置,标志,访问权限和对齐等信息

程序hello包括8个段:

PHDR 保存程序头表

INTERP 是程序从可执行文件映射到内存以后必须调用的解释器,如动态链接器

LOAD 表示一个需要从二进制文件映射到虚拟地址空间,保存了常量和目标代码的段,从第二个load段开始为读/写段,上面则是只读代码段

DYNAMIC 保存了动态链接器使用的信息

NOTE 保存辅助信息

GNU_STACK 标志栈是否是可执行的权限标志

GNU_RELRO 指定在重定位结束之后需要设置只读的内存区域

使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。

5.5 链接的重定位过程分析

objdump -d
-r hello 分析hello与hello.o的不同,说明链接的过程。

Hello.o的反汇编代码

Hello的反汇编代码

通过对比两份反汇编代码可以看出,hello与hello.o有以下不同

  1.  链接后hello的反汇编代码比hello.o的长很多,原因是链接过后hello中加入了hello.c中调用的其他函数,比如printf,exit,sleep等等
    
  2.  Hello.o中的相对偏移地址变成了hello中的虚拟内存地址,hello.o文件中对于全局变量的访问为$0x0和0(%rip),汇编成机器语言后将操作数全部置为0,并添加重定位条目
    
  3.  Hello中增加了.init和.plt节以及节中定义的函数
    
  4.  Hello无需重定位所以没有hello.o中的重定位条目,跳转地址和函数调用地址在hello中都变成了虚拟内存地址。链接器将hello.o中的偏移量加上程序在虚拟内存中的起始地址0x0040000和.text节中的偏移量就得到了反汇编代码hello中的地址
    

链接的过程为:

  1.  首先判断输入文件是不是库文件,如果不是则是目标文件f,目标文件放入集合E中
    
  2.  链接器解析目标文件中的符号,若出现未定义的符号则将其放入集合U,出现了定义但未使用的符号则放入集合D中
    
  3.  链接器读入crt*库1中的目标文件
    
  4.  接入动态链接库libc.so
    

由此我们可以分析重定位的步骤:

  1.  合并相同的节:链接器首先将所有相同类型的节合并成为同一类型的新节,例如所有文件的.data节合并成一个新的.data节,合并完成后该新节即为可执行文件hello的.data节。
    
  2.  确定地址:之后链接器再分配内存地址赋给新的聚合节和输入模块定义的节以及符号。地址确定后全局变量,指令等均具有唯一的运行时地址
    

3.符号引用:链接器根据汇编器给出的hello.o中的重定位条目,修改代码段和数据段中每个符号的引用,赋予他们正确的运行地址

具体的重定位分析举例:

我们以puts函数来举例分析重定位是如何实现的:

在这里插入图片描述

图 5.5.1 hello.o中调用puts函数语句

在这里插入图片描述

图5.5.2 hello反汇编代码中调用puts函数语句

两图对比我们可以看出在可重定位目标文件变成可执行文件时,原本空着的00 00 00 00 处已经被填上了5d ff ff ff,那么这个值是如何计算出来的呢?

在这里插入图片描述
图 5.5.3 puts函数

我们在hello反汇编中找到puts函数,puts函数首地址是4004b0,离PC

目前指向的地址(即call语句的下一条地址)400553相差-0000a3,即ffffff5d,再根据小端法,在原处填上5d ff ff ff即可

结合hello.o的重定位项目,分析hello中对其怎么重定位的。

5.6 hello的执行流程

使用rbreak指令在每个函数出设置断点,即能查看函数运行流程及其地址

CSAPP大作业-hello的一生_第18张图片
CSAPP大作业-hello的一生_第19张图片
图5.6 函数名称

使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。

5.7 Hello的动态链接分析

在这里插入图片描述


5.7.1GOT表的地址

找到GOT表的地址为0x0000000000600ff0

通过edb的datadump窗口定位到GOT表处,在do_init前后设置断点,run this line观察GOT表的变化

在这里插入图片描述

图 5.7.2 do_init前GOT表

CSAPP大作业-hello的一生_第20张图片

图5.7.3 do_init后GOT表的变化

dl_init调用之后0x00600ff0 处和0x00601000 处的8bit数据分别变为了b09a83edfd7e 和70 21 e3 ed fd 7e,GOT[1]指向重定位表(.plt节需要重定位的函数运行时地址),作用是确定调用函数的地址,GOT[2]指向动态链接器ld-linux.so运行时地址

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

5.8 本章小结

本章简介了linux系统下链接的过程,链接是程序变成可执行文件的最后一步,经过链接将各种代码段和数据段整合到一起后,hello已经准备好接受系统的安排,准备上岗工作了。

通过查看hello的虚拟地址空间,对比hello.o和hello的反汇编代码等等一系列的分析过程,对重定位,执行流程和动态链接过程有了一个大致的概括,由此对于链接的过程有了更加深刻的了解。链接看似只是一个简单的合并过程,实际上却发挥着很大的作用,在软件开发中扮演着关键的角色,如果没有链接,编译修改大型程序可能要付出巨大的代价。

(第5章1分)

第6章 hello进程管理

6.1 进程的概念与作用

进程的概念:

  1.  广义上的概念:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动
    
  2.  狭义上的概念:进程是一个执行者中程序的实例
    

通常我们谈论进程的时候指的是狭义上的进程概念,系统的每个程序都运行在某个进程的上下文中,而上下文是由程序正确运行所需的状态组成的,包括存放在内存中的程序代码和数据,程序的栈,通用目的寄存器,程序计数器,环境变量和打开文件描述符的集合,我们可以将进程理解为正在执行中的程序。

进程的作用:

  1.  由于内存资源和cpu资源有限,进程最重要的作用就是给应用程序制造一个假象,仿佛该程序在独占处理器,拥有私有的地址空间,占用独立的资源
    
  2.  假如我们需要执行某个程序,在linux中我们需要通过shell这个父进程来创建子进程,从而在该进程的上下文中运行这个可执行目标文件。
    

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

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

Shell:本质是一个命令行解释器,用户通过shell与unix系统进行交互,这一点我们也可以从它的名字来理解,shell即linux的外壳。由于安全性,复杂性等原因,用户要控制计算机硬件,但真正能够控制计算机硬件的只有计算机内核,出于安全等原因,需要shell这样一个程序来做用户和内核的沟通桥梁。

Bash是linux的默认配置shell

Shell-bash处理命令行流程:

1.将命令行分成由元字符分隔的记号

元字符包括SPACE,TAB,NEWLINE,,),<,>,|,&

记号类型:单词,关键字,I/O重定向符号

2.检测每个命令的第一个记号,判断是否为不带引号或反斜线的关键字,如果是开放的关键字,则shell将会对此复合命令进行处理,读取下一个命令并重复这一流程。如果不是,则提示语法错误

3.依据别名列表检查每个命令的第一个关键字,如果找到匹配则替换别名定义,返回第一步,否则进入第四步

4.执行大括号扩展,如a(b,c)变为ab ac

5.如果位于单词开头用$HOME替换

6.对所有以$开头的表达式执行参数替换

  1. 对形如$(string)或者string 的表达式进行命令替换 ,这里是嵌套的命令行处理。

  2. 计算形式为$((string))的算术表达式

9.替换行参数,命令替换和算术替换的结果部分再次分为单词,使用$IFS中的字符做分割符而不是步骤1的元字符集

10.对*,?[]执行路径名扩展

11.按命令优先级表进行命令查询

12.设置完I/O重定向和其他操作后执行该命令

Shell的例程:

  1.  打印一个命令行提示符,等待用户在stdin上输入命令行,对该命令行求值
    
  2.  对命令行求值的首要任务是调用parseline函数,该函数解析以空格分隔的命令行参数,并构造最终会传递给execve的argv向量。第一个参数被假设为要么是一个内置的shell命令名,马上会解释这个命令,要么是一个可执行目标文件,会在一个新子进程的上下文中加载并运行这个文件
    
  3.  如果最后一个参数是一个&,则parseline返回1,表示在后台执行该程序(shell不会等待它完成),否则返回0,表示在前台执行该程序(shell会等待它完成)
    
  4.  解释命令行后eval函数将调用builtin_command函数,该函数检查第一个命令行参数是否是一个简单的内置shell命令,如果是立即解释这个命令并返回1,否则返回0(内置命令有quot,bg,fg,pwd,jobs等)
    
  5.  如果builtin_command返回0,那么shell创建一个子进程,并在子进程中执行所请求的程序,如果用户要求在后台运行该程序,那么shell返回循环底部等待下一个命令行,否则shell使用waitpid等待作业终止,终止后shell开始下一轮迭代
    

6.3 Hello的fork进程创建过程

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

根据shell的处理流程,fork创建进程有以下几步:

  1.  输入./hello,shell读入命令
    
  2.  父进程通过fork函数创建新的运行子进程hello
    

Fork函数:pid_t fork(void);

父进程调用一次fork函数但有两次返回,一次返回到父进程一次返回到子进程

  1.  父进程用waitpid函数等待子进程终止或停止
    
  2.  在子进程停止之前,父进程执行的操作和子进程的操作在时间顺序上是拓扑排序执行的,这段时间内父子进程的逻辑控制流指令交替进行
    

新子进程的特点:

  1.  子进程几乎但不完全与父进程相同,子进程拥有与父进程用户级虚拟地址空间相同但独立的一份副本,包括代码,数据段,堆,共享库以及用户栈
    
  2.  子进程拥有与父进程任何打开文件描述符相同的副本,意味着子进程可以读写父进程中打开的任何文件
    
  3.  子进程与父进程最大差别在于他们有不同的PID
    
  4.  父进程和子进程是并发运行的独立进程
    

CSAPP大作业-hello的一生_第21张图片

图6.3 fork过程

Fork函数在新的子进程中运行相同的程序,新的子进程是父进程的一个复制品。

6.4 Hello的execve过程

Execve函数的原型为: int execve(const char *filename,const char
argv[],const char envp[])

作用:execve()用来执行参数filename字符串所代表的文件路径,第二个参数利用指针数组来传递给执行文件,并且需要以空指针NULL结束,最后一个参数为传递给执行文件的新环境变量数组,其中每个指针指向一个环境变量字符串,每个串都是形如name=value的名字-值对。当execve加载了filename之后,它调用启动代码,启动代码设置栈,将控制传递给新程序的主函数main。当出现例如找不到filename的错误,execve将返回调用程序,与fork调用一次返回两次不一样,execve调用一次并从不返回。

当main开始执行时,用户栈的组织结构如图所示
CSAPP大作业-hello的一生_第22张图片

图 6.4.1 一个新程序开始时用户栈的典型组织结构

栈低往栈顶依次是参数,环境字符串,以null结尾的指针数组(每个指针指向栈中的环境变量字符串),以null结尾的argv[]数组,其中每个元素都指向栈中的一个参数字符串,在栈的顶部是系统启动函数libc_start_main的栈帧。

Execve函数在当前进程的上下文中加载并运行一个新的程序,它会覆盖当前进程的地址空间,但并没有创建新的进程,新的程序仍然与旧程序具有相同的pid,并继承了调用execve函数时已打开的所有文件描述符

6.5 Hello的进程执行

结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。

逻辑控制流:进程向每个程序提供一种假象,好像它在单独的占用处理器,实际上每个进程都是轮流使用处理器的。每个进程执行它流的一部分,然后被抢占(暂时挂起),然后轮到其他进程。
CSAPP大作业-hello的一生_第23张图片
图6.5.1 逻辑控制流

时间片:指的是一个进程执行它的控制流的一部分的每一时间段,多任务也称为时间分片

用户模式和内核模式:处理器通过用某个控制寄存器的模式位实现限制一个应用可以执行的指令以及它可以访问的地址空间范围的功能,该寄存器描述了进程当前享有的特权,当设置模式位时,进程运行在内核模式中,一个运行在内核模式中的进程可以执行指令集的任何指令,并且可以访问系统中的任何内存位置。

当没有设置模式位时,进程运行在用户模式中,用户模式中的进程不允许执行特权指令,例如停止处理器,改变模式位,不允许进程直接引用地址空间中内核区的代码段和数据,此时用户程序必须通过系统调用接口间接访问内核代码和数据。

CSAPP大作业-hello的一生_第24张图片

图6.5.2 上下文切换

上下文切换:操作系统内核使用一种称为上下文切换的异常控制流来实现多任务,内核为每个进程维持一个上下文,上下文由一些对象的值组成,这些对象包括通用目的寄存器,浮点寄存器,程序计数器,用户栈,状态寄存器,内核栈和内核数据结构。

1)保存当前进程的上下文

2)恢复某个先前被抢占的进程被保存的上下文

3)将控制传递给这个新恢复的进程

引发上下文切换的因素有异常,系统调度等,我们可以将上下文切换理解为一个煮饭的人,他先煮上饭以后就将电饭煲放置,改去切菜,但如果电饭煲出现故障(类比异常),厨师就会紧急暂停切菜而去查看电饭煲的状态,直到电饭煲恢复正常以后才恢复切菜(返回原先进程)。

Hello的进程执行分析:

CSAPP大作业-hello的一生_第25张图片

图6.5.3 Hello的进程分析

  1.  hello初始运行在用户模式
    
  2.  hello进程调用sleep函数后进入内核模式,内核处理休眠请求主动释放当前进程
    
  3.  内核将hello进程移动到等待队列,进程控制权移交其他进程
    
  4.  等待2.5s后定时器发送中断信号,进入内核模式进行中断处理
    
  5.  将hello进程重新移动到运行队列,此时hello的上下文得到恢复,回到用户模式
    

6.6
hello的异常与信号处理

1.6.1
正常运行

CSAPP大作业-hello的一生_第26张图片

图6.6.1.1 正常运行 参数为3个

在这里插入图片描述

图 6.6.1.2 正常运行,参数不为3个

1.6.2
CTRL+C

在这里插入图片描述

图6.6.2.1 CRTL+C运行

Crtl+c指令向进程发送sigint信号让进程直接结束,此时再输入ps不能恢复它的运行

CSAPP大作业-hello的一生_第27张图片

图 6.6.2.2 CRTL+C后ps

6.6.3CTRL+Z

Crtl+z向进程发送了一个sigtstp信号让进程暂时挂起

在这里插入图片描述

图6.6.3.1CRTL+Z运行

当输入kill后进程被杀死,此时再输入ps指令后发现当前无进程执行

在这里插入图片描述

图 6.6.3.2 kill指令

CSAPP大作业-hello的一生_第28张图片

未输入kill时输入ps指令后发现此时hello进程尚未关闭

图 6.6.3.3 ps指令

在这里插入图片描述

图 6.6.3.4 jobs指令

Jobs指令的功能是列出暂停的进程

CSAPP大作业-hello的一生_第29张图片

图 6.6.3.5 pstree指令

Pstree以树状图显示进程间的关系

CSAPP大作业-hello的一生_第30张图片

图6.6.3.6 fg指令

空格是运行时按下了回车的结果

Fg的功能是将挂起的hello继续执行

CSAPP大作业-hello的一生_第31张图片

图6.6.4 hello运行时乱按

在hello运行时乱按键盘,不好影响进程执行,getchar()会把缓冲区里的这些输入发送给shell,在hello结束后读入

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

hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。

程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs
pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。

6.7本章小结

在本章中hello已经正式上岗工作运行了,这都多亏于“进程”这个概念的提出。进程给hello提供了抽象的概念,使得进程能够有条不紊的并发执行。各个进程之间也不会产生严重的矛盾和冲突,是进程使得不同的程序能在相安无事的运行直到结束。

本章主要介绍了进程的概念和作用,描述了shell如何在用户和系统内核之间搭建桥梁,介绍了fork函数和execve函数在调用程序中的作用,程序的异常如何处理,不同进程之间是如何进行上下文切换等等内容。

(第6章1分)

第7章 hello的存储管理

7.1 hello的存储器地址空间

物理地址:物理地址是内存单元的绝对地址,与地址总线具有对应关系。无论CPU如何处理地址,最终访问的都是物理地址。CPU实模式下段地址+段内偏移地址即为物理地址,CPU可以使用此地址直接访问内存。物理地址的大小决定了内存中有多少个内存单元,物理地址的大小由地址总线位宽决定。

线性地址(虚拟地址):CPU在保护模式下,“段基址+段内偏移地址”为线性地址,如果CPU在保护模式下未开启分页功能,线性地址将被当成物理地址使用。若开启了虚拟分页功能,线性地址等同于虚拟地址,此时虚拟地址需要通过页部件电路转化为最终的物理地址。虚拟地址是CPU由N=2n个地址空间中生成的,虚拟地址即为虚拟空间中的地址

逻辑地址:无论cpu在什么模式下,段内偏移地址又称为有效地址/逻辑地址

Hello中的指令地址都是16位的虚拟地址,在程序中虚拟地址和逻辑地址没有明显的界限。

逻辑地址转换成线性地址(虚拟地址),由段式管理执行的

线性地址转换成物理地址,是由页式管理执行的

CSAPP大作业-hello的一生_第32张图片

图7.1 地址之间的转换关系

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

结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。

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

段式管理:指的是把一个程序分成若干个段进行存储,每个段都是一个逻辑实体。段式管理是通过段表进行的,包括段号(段名),段起点,装入位,段的长度等。程序通过分段划分为多个块,如代码段,数据段,共享段等。

逻辑地址是程序源码编译后所形成的跟实际内存没有直接联系的地址,即在不同的机器上使用相同的编译器来编译同一个源程序则其逻辑地址是相同的,但在不同机器上生成的线性地址是不相同的。

一个逻辑地址是两部分组成的,包括段标识符和段内偏移量。段标识符是由一个16位长的字段组成的,称为段选择符。前13位是一个索引号,后3位为一些硬件细节。如图,索引号即是“段描述符”的索引,段描述符具体地址描述了一个段,很多个段描述符就组成了段描述符表。通过段标识符的前13位直接在段描述符表中找到一个具体的段描述符。

在这里插入图片描述

图 7.2.1 段选择符

CSAPP大作业-hello的一生_第33张图片

图7.2.2 段描述符

CSAPP大作业-hello的一生_第34张图片
图 7.2.3 逻辑地址到线性地址的转换

图中的Base字段描述了一个段的开始位置的线性地址

GDT:全局段描述符表,存放全局的段描述符

LDT:局部段描述符表,存放局部的段描述符

逻辑地址转化为线性地址的过程:

先检查段选择符的T1字段,该字段决定了段描述符保存在哪一个描述符表中,(比如转换的是GDT还是LDT中的段)。如果是GDT中的段(T1=0),分段单元从gdtr寄存器中得到GDT中的线性基地址。如果是LDT中的段(T1=1),分段单元从ldtr寄存器中得到GDT的线性基地址。之后再根据相应寄存器得到地址和大小

由于一个段描述符字长为8bits,其在GDT或LDT中的相对地址是段选择符的最高13位的值×8。此时我们就得知了其偏移地址。

Base+offset得到线性地址

逻辑地址转化为线性地址的公式:线性地址 = 段基址*16+偏移的逻辑地址

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

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

页:线性地址被划分为固定长度单位的数组,例如32位的机器线性地址为4G,用4KB为一个页来划分。整个线性地址被划分为一个220的数组,共有2的20次方个页,称为页表。

物理页:也称页框,页桢,是分页单元将所有物理内存划分成固定大小的单元为管理单位,通常情况下其大小与内存页大小一致。

为了节约维护进程的内存空间,cpu在页式内存管理方式中引入了二级页表结构,第一级页表称作页目录,用于存放页表的基地址,第二级页表用于存放物理内存中页框的基地址

CSAPP大作业-hello的一生_第35张图片

图7.3.1 二级页表结构

线性地址转化为物理地址的过程如下:

从cpu寄存器CR3中取出进程的页目录地址,操作系统负责调度进程时将该地址装入对应寄存器,取出其前20位,为页目录的基地址。

根据线性地址前10位(面目录索引),在数组中找到对应的索引项

根据线性地址的中间10位(页表索引),在页表中找到页的起始地址

将页的起始地址与线性地址中最后12位(偏移)相加即得到物理地址

如何利用页表实现虚拟地址到物理地址的映射:

CPU中的页表基址寄存器指向当前页表。N位虚拟地址包括一个p位的虚拟页面偏移(VPO)和一个n-p位的虚拟页号(VPN)。

内存管理单元MMU利用VPN选择适当的PTE

将页表条目中的物理页号(PPN)和虚拟地址中的(VPO)串联起来即得到物理地址

需要注意的是因为虚拟页面和物理页面都是p字节的,所以物理页面偏移PPO和VPO相同

CSAPP大作业-hello的一生_第36张图片
图7.3.2 虚拟地址和物理地址的转换

二级页表机制的优点:

二级表结构页表分散在内存的各个页面中,不需要保存在连续的4M内存块中

不需要位不存在的线性地址空间分配二级页表,使得页表结构大小对应于实际使用的线性地址大小

页目录和页表中的每个表项都有一个存在属性,页目录中的存在熟悉指明对应的页表结构是否存在。如果页目录指明对应二级页表存在,那么通过访问二级表,进行通常的查找;如果存在标志表明对应的二级表项不存在,处理器将产生缺页异常来通知操作系统。由于二级页表不存在而引发的异常会通知操作系统将缺少的页表从磁盘加载进物理内存,将页表存储在虚拟内存中减少了保存页表需要的物理内存。

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

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

Core i7采用四级页表层次结构,CR3控制寄存器指向第一级页表L1的起始位置,CR3的值是每个进程上下文的一部分,每次上下文切换时CR3的值都会被恢复

CSAPP大作业-hello的一生_第37张图片

图7.4.1 第一级,第二级,第三级,页表条目格式

P = 1时地址字段包含一个40位的物理页号PPN,它指向适当的页表开始处

CSAPP大作业-hello的一生_第38张图片

图7.4.2 第四级页表条目的格式

第四级页表条目格式,P=1时地址字段包括一个40位PPN,指向物理内存中某一页的基地址

CSAPP大作业-hello的一生_第39张图片

图 7.4.3 core i7地址翻译的概况

下图给出了core i7MMU如何使用四级页表来将虚拟地址翻译成物理地址,36位的VPN被划分为四个9位的片,每个片被用作到一个页表的偏移量,CR3寄存器包含L1页表的物理地址,VPN1提供到一个L1 PET 的偏移量,这个PTE包含L2页表的基地址。VPN2提供到一个 L2 PTE 的偏移量,以此类推。

CSAPP大作业-hello的一生_第40张图片

图 7.4.4 core i7页表翻译

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

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

Intel core i7处理器的高速缓存层次结构如图,每个cpu芯片有四个核,每个核有自己私有的L1 i-cache,所有的SRAM高速缓存存储器都在CPU芯片上

CSAPP大作业-hello的一生_第41张图片

图7.5.1 Inter core i7高速缓存层次结构

CSAPP大作业-hello的一生_第42张图片

图7.5.2 高速缓存通用组织

如图,高速缓存cache是一个高速缓存组的数组,共有S个组,每组包含E行,每行有一个有效位,t个标记位和log2B位的数据块

物理内存访问基于MMU将虚拟地址翻译成物理地址以后向cache中访问的,在cache中进行物理寻址有三个步骤:

  1.  组选择:cache按照物理地址的s个索引位来定位该地址映射的组
    
  2.  行匹配:选择组以后遍历组中的每一行,比较行的标记和地址标记,当二者相同且有效位为1时,该行包含地址的一个副本,此时缓存命中,否则缓存不命中,需要从下一层cache中取出请求块,将新的块存入高速缓存中,如果发生冲突不命中则需进行行替换,通常采用最近最少使用策略LFU算法进行替换
    
  3.  字选择:定位了行以后根据地址的块偏移量在行的数据块中进行寻址,得到的字即为地址指示的字
    

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

1.6
hello进程fork时的内存映射

进程能为每个进程提供自己私有的虚拟地址空间,免受其他进程的错误读写。但许多进程有相同的代码区域,比如printf函数这种来自于标准语言库的函数,假如每个进程中都保存一份同样的副本,将会是对内存的一种极大的浪费,而内存映射为我们提供了共享对象这种机制来节约内存开销。

CSAPP大作业-hello的一生_第43张图片

图7.6 写时复制

基于共享对象的概念我们可以试图去理解fork函数是如何创建一个带有自己独立虚拟地址空间的新进程:

  1.  fork被当前进程调用时,内核为新进程创建数据结构,分配唯一的PID
    
  2.  fork创建了当前进程的mm_struct(内存描述符,描述一个进程的整个虚拟内存空间),区域结构和页表的原样副本用来给新的进程创建虚拟内存
    
  3.  fork将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制
    
  4.  fork在新进程中返回时,新进程现在的虚拟内存与调用fork时存在的虚拟内存相同,这两个进程任意一个进行写操作时,写时复制机制将创建新页面,为每个进程保持了私有地址空间的概念
    

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

1.7
hello进程execve时的内存映射

execve函数如何加载和执行程序hello.out:

执行execve(”hello.out”,NULL,NULL);的execve调用

删除已存在的用户区域:删除当前进程虚拟地址的用户部分中已存在的区域结构

映射私有区域:为新程序的代码,数据,bss和栈区创建新的区域结构,这些新区域结构都是私有,写时复制的。代码和数据区域被映射为hello.out文件中的.text和.data区,bss区域是请求二进制0的,映射到匿名文件,其大小包括在hello.out中。栈和堆区域也是请求二进制0的,初始长度为0

映射共享区域:如果hello.out程序与共享对象/目标进行链接,比如标准c库libc.so,这些对象都是动态链接到该程序然后再映射到用户虚拟地址空间中的共享区域内

设置程序计数器PC:execve设置当前进程上下文中的程序计数器使之指向代码区域的入口点

CSAPP大作业-hello的一生_第44张图片

图7.7 加载器是如何映射用户地址空间区域的

下次再调用hello进程时它将从这个入口点开始执行

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

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

MMU在试图翻译某个虚拟地址A时触发了一个缺页,这个异常导致控制转移到内核的缺页处理程序,程序将进行以下几个判断:

虚拟地址A是否合法:缺页处理程序搜索区域结构的链表,将A和每个区域结构中的vm_start和vm_end比较,如果指令不合法,程序触发一个段错误,终止进程,标识为1

试图进行的内存访问是否合法?如果试图进行的访问不合法,程序会触发一个保护异常,终止进程,标识为2

如果不是由于以上两个原因,那么造成缺页异常的访问是合法的,接下来程序选择一个牺牲页面,如果这个页面被修改过则将其交换出去,换入新页面并更新页表,当缺页处理程序返回时CPU重新启动引起缺页的指令,再次发送A到MMU,之后A将会被正常翻译

CSAPP大作业-hello的一生_第45张图片

图 9.8 缺页处理

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

7.9动态存储分配管理

动态内存分配器维护着一个进程的虚拟内存区域,称为堆

CSAPP大作业-hello的一生_第46张图片

图7.9 堆

假设堆是一个请求二进制0的区域,它紧接着在未初始化的数据区域后开始,并向上生长。对于每个进程,内核维护着变量brk(break),指向堆的顶部

分配器将堆视作一组不同大小的块的集合来维护,每个块都是一个已分配或空闲的连续的虚拟内存片。已分配的块显示保留给应用程序使用,空闲块则用来分配。块可以在已分配和空闲两种状态之间相互转化。

分配器的种类:

显式分配器:要求应用显示释放任何已分配的块,c语言通过malloc函数来分配一个块,通过free函数释放一个块

隐式分配器:要求分配器检测一个已分配块何时不再使用,那么释放该块,也称作垃圾收集器,例如Lisp,ML,Java之类的语言采用这种策略

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

7.9.1 隐式空闲链表

隐式空闲链表的一个块是由一个字的头部,有效荷载,以及一些额外的填充组织组成。头部编码了这个块的大小(包括头部和所有的填充),以及这个块是已分配的还是空闲的。如果加上一个双字的对齐约束条件,块的大小就总是8的倍数,且块大小的最低3位总是0。

头部后面就是应用调用malloc时请求的有效荷载,有效荷载后面是一片不使用的填充块,大小任意。
CSAPP大作业-hello的一生_第47张图片

 图7.9.9.1 堆的数据结构

CSAPP大作业-hello的一生_第48张图片

图7.9.9.2 用隐式空闲链表来组织堆

空闲块通过头部中的大小字段隐含连接,分配器通过遍历堆中所有的块从而间接遍历整个空闲块的集合。隐式空闲链表的优点是简单,缺点是开销较大

隐式空闲链表分配内存的步骤::

  1.  放置已分配的块:应用请求块后分配器搜索空闲链表,查找足够大的可以放置所请求块的空闲块。执行搜索的方式有首次适配,下一次适配,最佳适配
    
  2.  分割空闲块:找到空闲块后决定占用多大的空闲块,大小不合适时将其分割
    
  3.  获取额外的堆内存:当找不到合适的空闲块时将向内存请求额外的堆内存
    
  4.  合并空闲块:将碎片化的空闲块进行合并提高内存利用率,策略有使用标记的边界合并。我们可以将前面块已分配/空闲位存放在当前块中多出来的低位中,那么已分配的块就不再需要脚部,这块空间可以用作有效荷载
    

CSAPP大作业-hello的一生_第49张图片

图7.9.9.3 使用边界的标记合并

7.9.3 显式空闲链表

比隐式空闲链表更好的方式是将空闲块组织为某种形式的显示数据结构,根据定义,程序不需要一个空闲块的主体,实现这个数据结构的指针可以存放在这些空闲块的主体里。堆可以组织成一个双向空闲链表,在每个空闲块中都包含一个pred和succ指针。使用双向链表能使得首次适配的分配时间从块总数的线性时间减少到空闲块数量的线性时间。

CSAPP大作业-hello的一生_第50张图片

图7.9.3 使用双向空闲链表的堆块格式

空闲链表中块的排序策略:

  1.  后进先出(LIFO):将新释放的块放置在链表开始处,使用LIFO的顺序和首次适配的放置策略,此时分配器会最先检查最近使用的块,释放一个块可以在常熟时间内完成
    
  2.  按照地址顺序维护空闲链表:这种情况下释放一个块需要线性时间搜索定位合适的前驱。按照地址排序的首次适配比LIFO排序的首次适配有更高的内存利用率,接近最佳适配的利用率
    

显式链表的缺点是空闲块必须足够大以包含所有需要的指针以及可能的头部和脚部。

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

7.10本章小结

本章简要介绍了hello是如何存储在内存系统中的。相关内容有系统虚拟地址,物理地址,线性地址,逻辑地址之间的关系和相互转化,动态内存分配的不同策略,访问内存过程中如何处理故障,系统如何为hello创建进程和分配空间等内容。

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

(第7章 2分)

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:文件:一个linux文件就是一个m字节的序列,所有I/O设备都被模型化为文件,所有的输入和输出都被当作对应文件的读和写来执行。

文件具有以下几种类型:

  1.  普通文件:包含任意数据,分为文本文件和二进制文件
    
  2.  目录:包含一组链接的文件,每个链接都将一个文件名映射到一个文件
    
  3.  套接字:用来与另外一个进程进行跨网络通信的文件
    
  4.  其他文件:命名通道,符号链接,字符和块设备
    

设备管理:unix io接口:使得所有的输入和输出都能以统一且一致的方式进行。有以下几种操作:

  1.  打开文件:应用程序通过要求内核打开相应文件来宣告它想要访问一个I/O设备
    
  2.  改变当前文件的位置:应用程序通过执行seek操作显式设置文件当前位置为k
    
  3.  读写文件:读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n
    

写操作就是从内存复制n>0个字节到一个文件,从当前文件位置k开始然后更新k

4.关闭文件:内核收到关闭文件的请求后就释放文件打开时创建的数据结构并将这个描述符恢复到可用的描述符池中

8.2 简述Unix
IO接口及其函数

8.2.1 打开和关闭文件:

进程是通过调用open函数来打开一个已存在的文件或者创建一个文件的

在这里插入图片描述
图8.2.1.1 open函数

Open函数将filename转换为一个文件描述符,并返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符。

Flag参数指明了进程将如何访问这个文件:

O_DONLY:只读

O_WRONLY:只写

O_RDWR:可读可写

O_CREAT:如果文件不存在就创建它的一个截断的文件

O_TRUNC:如果文件已经存在就截断它

O_APPEND:每次写操作前设置文件位置到文件结尾处

Close函数:进程通过调用close函数关闭一个打开的文件,关闭一个已经关闭的描述符会出错

在这里插入图片描述
图8.2.1.2close函数

8.2.2 读写文件

应用程序通过调用read和write函数来执行输入和输出

CSAPP大作业-hello的一生_第51张图片

图8.2.2 read函数和write函数

read的函数从描述符为fd的当前文件位置复制最多n个字节到内存位置buf,返回值-1表示一个错误,返回值0表示EOF,除此之外返回值表示实际传送的字节数量

write函数:从内存位置buf复制至多n个字节到描述符fd的当前文件位置

lseek函数:应用程序用该函数显式修改当前文件位置

8.3 printf的实现分析

CSAPP大作业-hello的一生_第52张图片

图8.3.1 printf函数体

Printf函数在stdio.h头文件中声明,参数包括字符串fmt和…

…处表示的参数不能确定

Arg变量定位到第二个参数,即为第一个格式串。

Va_list是一个数据类型,具体声明:typedef char *va_list

赋值语句右侧为地址偏移定位,定位到从fmt开始之后的第一个char*变量,即为第二个参数

函数中调用了两个外部函数vsprintf和write

CSAPP大作业-hello的一生_第53张图片

图8.3.2 vsprintf函数

该函数的作用是将所有参数格式化之后存入buf,返回格式化数组的长度

系统函数write将buf中i个元素写到终端

https://www.cnblogs.com/pianist/p/3315801.html

从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.

字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。

显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

Getchar函数原型:

int
getchar(void)

{

char c;

return
(read(0,&c,1)==1)?(unsigned char)c:EOF

}

Getchar函数通过调用read函数返回字符,read函数的第一个参数是描述符fd,0代表标准输入,第二个参数输入内容的指针,最后一个参数是1,代表读入一个字符。

Getchar有一个int类型的返回值,程序调用getchar时:

  1.  程序等待用户按键,用户输入的字符存放在键盘缓冲区中,直到用户按回车为止。(回车放在缓冲区中)
    
  2.  之后getchar开始从stdin流中每次读入一个字符
    
  3.  Getchar函数返回用户输入的第一个字符的ASCII码,如果出错返回-1且将用户输入的字符显示在品名是
    
  4.  如果用户按回车之前输入不止一个字符,其他字符将会保留在键盘缓冲区中,等待后续getchar调用读取
    

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。

getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

本章介绍了hello是如何通过键盘输入和如何在屏幕上呈现的。同时对LinuxI/O设备管理机制以及unix接口函数进行了简要介绍,对c语言库函数printf和getchar的实现进行了简要分析

结论

随着文章的结束,hello坎坷的一生也到此结束了。虽然在人们眼中只是很短暂的时间,对于hello来说却是整个人生,让我们来回顾一下hello从诞生到消亡都经历了哪些过程:

1.源代码输入:hello的源代码hello.c通过键盘等外部设备输入系统并存储

2.预处理:预处理器对hello.c进行预处理得到hello.i,增加了一些附件让hello能更好的进行下一步

3.编译;将预处理后的文本文件hello.i进行语法分析优化等生成汇编文件hello.s

4.汇编:将汇编文件hello.s翻译成机器能读懂的机器代码hello.o

5.链接:链接器将hello.o与其他所需的库文件进行链接合并,生成可执行文件hello

6.进程管理和存储管理:系统提供给hello一个工作岗位,安排工作时间,即私有的地址空间和进程执行的时间,最后hello执行完以后被回收,结束了它的一生

7.I/O管理:hello与用户的接触都是在这一阶段,在键盘键入,屏幕输出hello的执行

感悟:写大作业的过程就是对书本内容复习的过程,不得不说这份作业对复习是很有帮助的,佩服设计作业的老师。

这门课本身主要介绍了程序实现背后的一些底层机制,说实话其实挺枯燥抽象的,而且可能因为是外文课本翻译的原因书本内容其实很难懂,通常第一遍看下来感觉什么也没懂就像看天书…之后多读才会稍好一点。但这份作业很好的将书本内容串联起来,与实践结合也让知识不再那么抽象,让我对书本知识有了更深刻的理解。了解程序实现的背后机制也能更好的写出机器友好的源代码。

思考:这份大作业是我在考试之前花了三天时间完成的,很惭愧的说这门课程学的实在是不够好,开学的时候觉得这门课的课本看不懂老师讲课也听不懂实验也不会做,一度花的心思很少,但是在期末复习看书第二遍乃至第三遍的时候终于渐渐领悟到了课本的内容,也在考完试以后回头把大作业写的时候的不足稍加完善。这门课的确是非常精彩,非常优秀的一门课,只可惜个人能力有限,耐心不足,无法完全学懂学透,以后的学习中我想还是应该多点耐心和钻研的精神,而不是一遇到困难就退缩偷懒。

创新理念:谁也不知道日后计算机会向什么方向发展,目前摩尔定律已经有失效的趋势,机器能做到的终究是有限的。要想有更大的突破也许应该从人脑思考的过程寻找答案?

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

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

附件

Hello.c Hello的c语言代码

Hello.i 预处理之后的文本文件

Hello.s 编译之后产生的汇编文件

Hello.ld 链接后的文件

Hello.o 可重定位的目标文件

Hello 可执行文件hello

Helloo.objdump Hello.o反汇编文件

Hello.objdump Hello的反汇编文件

列出所有的中间产物的文件名,并予以说明起作用。

参考文献

为完成本次大作业你翻阅的书籍与网站等

[1]  c语言预处理程序百度百科:https://baike.baidu.com/item/C语言预处理程序/22057237?fr=aladdin

[2]  C语言的预处理详解:https://blog.csdn.net/czc1997/article/details/81079498

[3]  ELF可重定位目标文件简析:

https://www.cnblogs.com/lyw-hunnu/p/11619512.html

[4]  程序的链接—重定位https://www.jianshu.com/p/3e3218ef0bcf

[5]  Linux下shell脚本:bash的介绍和使用(详细)https://blog.csdn.net/weixin_42432281/article/details/88392219

[6] Bash命令行处理流程详解

https://blog.csdn.net/weixin_34148340/article/details/91912492

[7]execve百度百科 https://baike.baidu.com/item/execve/4475693?fr=aladdin

[8] 通俗理解CPU中物理地址、逻辑地址、线性地址、虚拟地址、有效地址的区别https://blog.csdn.net/mzjmzjmzjmzj/article/details/84713351

[9]段式存储管理 百度百科https://baike.baidu.com/item/段式存储管理/7291432?fromtitle=%E6%AE%B5%E5%BC%8F%E7%AE%A1%E7%90%86&fromid=15988667&fr=aladdin

[10] 图示逻辑地址到线性地址的转换流程

https://www.cnblogs.com/image-eye/archive/2011/07/13/2105765.html

[11] linux2.6 内存管理—逻辑地址转化为线性地址(逻辑地址,线性地址,物理地址,虚拟地址)

https://www.cnblogs.com/diaohaiwei/p/5094959.html

[12]getchar的宏观实现:https://blog.csdn.net/scanferror/article/details/78647870

[13] 深入理解计算机系统
Randal E.Bryant  David R.O’Hallaron
机械工业出版社

你可能感兴趣的:(CSAPP大作业-hello的一生)