哈工大CSAPP大作业

 

 

                                          

 

计算机系统

 

大作业

 

 

题     目  程序人生-Hello’s P2P 

专       业       计算学部         

学     号         1190202128     

班     级         103002         

学       生          林搏海      

指 导 教 师        郑贵滨          

 

 

 

 

 

 

计算机科学与技术学院

2021年6月

摘  要

本文通过一个给定的程序hello.c,通过hello.c在Linux环境中被编译为可执行文件的步骤,逐步介绍在程序员视角下的计算机系统相关知识。例如汇编代码的产生,CPU的架构以及指令集设计,关于代码优化技术与多级缓存结构的结合等等,还有关于进程的基本概念和抽象,关于虚拟内存的概念和机制等等内容。

关键词:Ubuntu;汇编代码;进程;文件操作;CPU机制;虚拟内存机制                           

 

(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1

 

 

 

 

 

 

 

 

 

目  录

 

第1章 概述... - 5 -

1.1 Hello简介... - 5 -

1.1.1 P2P的过程阐述:... - 5 -

1.1.2 O2O的过程阐述... - 5 -

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

1.3 中间结果... - 6 -

1.4 本章小结... - 6 -

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

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

2.1.1 预处理的概念:... - 7 -

2.1.2预处理的作用:... - 7 -

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

2.2.1预处理指令:... - 7 -

2.2.2生成的文件展示:... - 8 -

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

2.4 本章小结... - 11 -

第3章 编译... - 12 -

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

3.1.1编译的概念... - 12 -

3.1.2编译的作用... - 12 -

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

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

3.3.1常量的解析... - 13 -

3.3.2变量的解析... - 14 -

3.3.3类型转换... - 14 -

3.3.4算数操作... - 14 -

3.3.5控制转移... - 15 -

3.3.6数组/指针/结构操作... - 15 -

3.3.7函数操作... - 16 -

3.4 本章小结... - 18 -

第4章 汇编... - 20 -

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

4.1.1汇编的概念... - 20 -

4.1.2汇编的作用... - 20 -

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

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

4.3.1readelf操作... - 21 -

4.3.2ELF头... - 22 -

4.3.3节头目表... - 23 -

4.3.4重定位节... - 24 -

4.3.5符号表... - 24 -

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

4.4.1 objdump操作... - 25 -

4.4.2汇编代码比较... - 27 -

4.5 本章小结... - 27 -

第5章 链接... - 29 -

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

5.1.1链接器的概念... - 29 -

5.1.2链接器的作用... - 29 -

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

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

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

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

5.5.1hello与hello.o的反汇编文件的不同... - 34 -

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

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

5.8 本章小结... - 35 -

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

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

6.1.1进程的概念... - 36 -

6.1.2进程的作用... - 36 -

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

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

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

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

6.5.1逻辑控制流... - 37 -

6.5.3上下文... - 37 -

6.5.4调度的过程... - 38 -

6.5.5用户态与核心态转换... - 38 -

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

6.6.1异常的类型和处理流程... - 38 -

6.6.2正常运行状态... - 40 -

6.6.3按下Ctrl+Z:... - 40 -

6.6.4按下Ctrl+C.. - 40 -

6.6.5中途乱按:... - 41 -

6.7本章小结... - 42 -

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

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

7.1.1逻辑地址... - 43 -

7.1.2线性地址... - 43 -

7.1.3虚拟地址... - 43 -

7.1.4物理地址... - 43 -

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

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

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

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

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

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

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

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

7.10本章小结... - 46 -

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

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

8.1.1设备的模型化... - 47 -

8.1.2设备管理... - 47 -

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

8.2.1Unix IO接口... - 47 -

8.2.2Unix IO函数... - 47 -

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

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

8.5本章小结... - 49 -

结论... - 49 -

附件... - 51 -

参考文献... - 52 -

 

 

 

第1章 概述

1.1 Hello简介

1.1.1 P2P的过程阐述:

P2P,全程Program to Process,其中的Program是指在编辑器中写下的hello.c文件。

而process则是指写下的程序经过预处理编译等操作,变为可执行文件的过程,具体过程为:cpp预处理器将hello.c文件中的#开头的命令进行预处理,插入对应的系统头文件中的内容,得到hello.i文件;然后编译器ccl将其翻译为汇编语言文件hello.s然后汇编器as负责将汇编语言翻译为机器指令代码,得到hello.o文件;最后,在链接阶段,链接器ld将hello.o和其它用到的预编译好的目标文件合并到一起并且完成引用的重定位工作,就得到了一个可执行文件hello。

在Ubuntu的shell程序中我们可以输入./hello执行hello程序,此时shell程序会调用fork函数创建子进程,并且在子进程中加载该程序,由此,hello.c文件就成为了一个进程。

1.1.2 O2O的过程阐述

在子进程调用execve函数加载hello执行后,操作系统OS为其映射虚拟内存至物理内存,CPU也为运行此进程分配时间,通过cache,TLB等机制加速访问时间,程序结束后,shell父进程通过相应的信号机制得到子进程结束的消息并且回收进程,同时内核将控制权转移回shell,子进程的相关数据被清除

1.2 环境与工具

硬件环境:

I7-9750H CPU  8g内存  983.58G硬盘  GTX1650显卡

软件环境:

VirtualBox 6.1.18  Ubuntu20.04.2LTS

开发工具:

VIM8.1.2269   Visual Studio Code1.54.3

1.3 中间结果

文件名

文件作用

hello.i

预处理后的ASCII码文件

hello.s

编译之后得到的文件

hello.o

汇编之后得到的目标文件

hello

链接之后得到的可执行文件

hello_disassemble.s

目标文件hello.o文件反汇编得到的结果

hello_objdumo.s

可执行文件hello反汇编之后的结果

helloELF.txt

目标文件hello.o的ELF格式

helloELF1.txt

可执行文件hello的ELF格式

1.4 本章小结

首先,本章介绍了关于hello.c的P2P和O2O机制,以及其中涉及到的一些具体技术举例,然后写明了完成本次大作业的硬件和软件环境,最后列举了完成作业过程中生成的所有的中间文件。

 

 

第2章 预处理

2.1 预处理的概念与作用

2.1.1 预处理的概念:

预处理是指程序在编译一个c文件之前,会调用cpp将所有#起始的行解释为预处理指令,并且对源代码进行相应的转换,包括加入包含的源文件,处理宏定义或者处理条件语句等等,预处理阶段也会删除注释等等。

2.1.2预处理的作用:

a.处理头文件,cpp预处理器可以将头文件插入对应指令的位置,从而修改源文件。

b.用实际值或者是代码去替换#define定义的宏。

c.#if等先关语句代表需要判断条件编译等情况。

2.2在Ubuntu下预处理的命令

2.2.1预处理指令:

指令如下:

 

生成文件如下:

2.2.2生成的文件展示:

 

2.3 Hello的预处理结果解析

我们可以发现,经过预处理之后,原本28行的代码被扩展为3065行,并且原本的注释也被删除,原本的代码被放置在3049-3065行,没有太大变化。

 

其中,stdio.h文件被插入在13-728行,unistd.h文件被插入在730-1966行,stdlib.h文件被插入在1969-3041行。

此外,我们发现所有调用的头文件地址如下:

 

在对应的目录下找到了所有的头文件

 

打开其中的stdio.h文件:

 

我们可以发现,在stdio.h文件中,依然存在着对其它头文件的调用,而这些头文件的调用也可以在最终的hello.i文件中被找到(如下图),因此,我们得出结论,cpp在处理针对头文件的调用时,是进行递归调用的,知道调用的头文件中再无别的头文件引用。

 

 

2.4 本章小结

本章主要介绍了预处理的概念以及相关功能,还有Linux系统下的预处理指令,通过对于得到的hello.i文本的分析,我们知道了cpp预处理器在执行预处理指令时候进行的具体工作:插入头文件,扩展宏定义,删除注释等等。其中,插入头文件的过程采取递归的方式,知道再也没有对于其它头文件的引用为止。

(第20.5分)

 

第3章 编译

3.1 编译的概念与作用

3.1.1编译的概念

编译程序所要作得工作就是通过词法分析、语法分析和语义分析等等,在确认所有的指令都符合语法规则之后,将其翻译成等价汇编代码。 在本次大作业中,我们需要让编译器将预处理之后得到的hello.i翻译成hello.s。

3.1.2编译的作用

编译主要是为了将高级语言(C,C++等)翻译为机器语言一一对应的汇编语言,具体步骤如下:

语法分析:首先,编译器会对程序代码进行分析和分割,形成一些C语言所允许的记号,同时会检查代码中是否生成了不规范记号,如果存在不规范表达就生成错误提示。然后分析语义并且生成符号表,将不符合语法规则的地方标出并且生成错误提示。

代码优化:将中间表示形式进行分析并转换为功能等价但是运行时间更短或占用资源更少的等价中间代码。

3.2 在Ubuntu下编译的命令

输入指令如下:

 

生成文件如下:

 

3.3 Hello的编译结果解析

3.3.1常量的解析

字符串常量

我们发现,hello.c文件中的字符串常量有两处:

 

 

在hello.s文件中我们可以找到常量字符串对应的存储位置:

 

其中我们发现汉字被以utf-8的格式进行编码,即每个汉字3字节,这一点在字符串的长度上也得到了体现(其中的中文感叹号也使用了utf-8编码,占3字节)

整形常量

整形常量在hello.c文件中是以代码中的数字的形式存在,例如:

 

 

这些常量被嵌入了汇编代码中:

 

 

3.3.2变量的解析

全局变量

其中的hello.c文件中的全局变量如下所示:

 

被放在了hello.s文件中的.data段

 

我们发现由于类型强制转换的缘故,编译器已经将其转换为了整形。

局部变量

在hello.c中出现的局部变量主要是int i

 

在汇编代码中可以发现其被分配在运行时栈上

 

3.3.3类型转换

其中的类型转换主要是用到了隐式类型转换,对于我们之前提到的全局变量

 

编译器直接将其初始化为2进行存储

 

3.3.4算数操作

在for循环中,int i进行了++操作

 

在对应的汇编代码中,是通过add指令完成的

 

3.3.5控制转移

Hello.c中的控制转移有对于表达式的值的判断,通过cmp指令和jle指令组合完成对于for循环体是否循环的判断,从而完成循环体的跳转。

3.3.6数组/指针/结构操作

在main函数中传递的参数出现了指针数组

每一个数组的元素都是一个char*类型的指针,可以指向一个字符串,而具体来说argv[0]指向文件名,argv[1] argv[2]分别指向命令行输入的第一个和第二个参数,在汇编代码中这个指针数组的首地址被存放在rsi中,也就是存放main函数的第二个参数的寄存器。

对于指针数组argv,其每一个元素的大小都是8字节,而且数组中的元素应是连续存放的,因此地址偏移量也应该是8的倍数

从此处的代码我们可以验证这一点。

3.3.7函数操作

Main函数

首先我们可以查看main函数的汇编代码,发现其两个参数,argc和argv分别被存放在寄存器rdi和rsi中,这与我们的课本知识吻合

继续查看后面的汇编代码,我们发现对于argv的操作,其指针数组的基地址通过寄存器存放,我们将基地址加上8的倍数的偏移量就可以访问指针数组的别的成员指针,而为了访问每一个指针成员指向的字符串,我们需要访问每一个指针指向的内存空间,也就是说,每个指针成员指向的字符串是存放在内存中的

Main函数不是由我们编写的函数调用的,是由系统函数调用,main函数可以调用别的函数,但是需要遵从寄存器保护的规则和参数传递的规则等等。

Main函数的返回值是int类型的,存储在寄存器rax中,在需要返回时,先将rax的值设置为0,然后返回即可,对应于hello.c文件中的return 0.

结合之前的代码我们可以发现,rax虽然被用作传递返回值,但是在此之前都可以被main函数自由使用。

Printf函数

Hello.c文件中,printf函数被调用了2次

第一次对应的汇编代码部分如下:

可以发现,第一次实际上是调用了puts函数,这是因为此次调用printf函数不需要传递额外的参数,只需要将内存中存储的字符串复制到屏幕上即可,因此编译器做了一点等价的替换。

第二次的对应汇编代码如下:

可以看到总共传递了三个参数,所以无法使用puts函数替换,第一个参数是主体字符串的首地址,被存放在rdi中,第二、第三个参数分别是替换的字符串的首地址,通过寄存器rsi和rdx传递,传递规则与课本中讲述的一致。

Exit函数

Exit函数的源代码如下:

对应的汇编代码如下:

我们可以看到函数传递的参数存放在edi中,对应了源代码中的exit(1),说明传递的整数值直接作为函数退出的状态值。

Sleep函数

Sleep函数的源代码如下:

对应的汇编代码如下:

可以看到sleep函数通过rdi传递了一个参数作为休眠时间的控制,需要注意的是,全局变量并非存储在运行时栈上,所以调用方式也有所不同

Getchar函数

Getchar函数的汇编代码如下:

由于此函数没有参数,因此不需要通过寄存器进行参数传递,。

3.4 本章小结

在本章主要了解了编译的概念,作用,以及过程。通过对于hello.i文件进行编译并且分析汇编代码,我们更加清楚地明白了全局变量和局部变量的区别,常量是如何进行存储的,对于非线性执行的跳转语句是如何进行的,以及在实现跳转的基础上如何进行逻辑控制,实现循环。以及在跳转的基础上,了解了函数是如何进行调用的,调用另一个函数的时候如何进行参数的传递,以及这个过程中怎么对寄存器的内容进行保护等等。

(第32分)

 

第4章 汇编

4.1 汇编的概念与作用

4.1.1汇编的概念

驱动程序运行汇编器as,将汇编语言翻译成机器语言的过程称为汇编,同时这个机器语言文件也是可重定位目标文件,在本次大作业中,此过程就是将hello.s文件翻译成hello.o文件。

4.1.2汇编的作用

汇编就是将高级语言转化为机器可直接识别执行的代码文件的过程,汇编器将汇编程序(本次大作业中的hello.s)翻译成机器语言指令目标文件(本次大作业中的hello.o),把这些指令打包成可重定位目标程序的格式,并将结果保存在.o 目标文件中,目标文件是一个二进制文件,它包含程序的指令编码。

4.2 在Ubuntu下汇编的命令

输入指令如下:

生成文件如下:

4.3 可重定位目标elf格式

4.3.1readelf操作

通过readelf指令将hello.o转换为TXT文档:

生成文件如下:

4.3.2ELF

其中,ELF头的信息如下:

ELF 头:

  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00

  类别:                              ELF64

  数据:                              2 补码,小端序 (little endian)

  Version:                           1 (current)

  OS/ABI:                            UNIX - System V

  ABI 版本:                          0

  类型:                              REL (可重定位文件)

  系统架构:                          Advanced Micro Devices X86-64

  版本:                              0x1

  入口点地址:               0x0

  程序头起点:          0 (bytes into file)

  Start of section headers:          1232 (bytes into file)

  标志:             0x0

  Size of this header:               64 (bytes)

  Size of program headers:           0 (bytes)

  Number of program headers:         0

  Size of section headers:           64 (bytes)

  Number of section headers:         14

  Section header string table index: 13

可以看到,其中包含了包含了版本和系统信息、编码方式、节的大小和数量、ELF头大小和头部索引等等一系列信息。

4.3.3节头目表

节头目表如下所示:

节头:

  [号] 名称              类型             地址              偏移量

       大小              全体大小          旗标   链接   信息   对齐

  [ 0]                   NULL             0000000000000000  00000000

       0000000000000000  0000000000000000           0     0     0

  [ 1] .text             PROGBITS         0000000000000000  00000040

       0000000000000085  0000000000000000  AX       0     0     1

  [ 2] .rela.text        RELA             0000000000000000  00000380

       00000000000000c0  0000000000000018   I      11     1     8

  [ 3] .data             PROGBITS         0000000000000000  000000c8

       0000000000000004  0000000000000000  WA       0     0     4

  [ 4] .bss              NOBITS           0000000000000000  000000cc

       0000000000000000  0000000000000000  WA       0     0     1

  [ 5] .rodata           PROGBITS         0000000000000000  000000cc

       000000000000002b  0000000000000000   A       0     0     1

  [ 6] .comment          PROGBITS         0000000000000000  000000f7

       000000000000002b  0000000000000001  MS       0     0     1

  [ 7] .note.GNU-stack   PROGBITS         0000000000000000  00000122

       0000000000000000  0000000000000000           0     0     1

  [ 8] .note.gnu.propert NOTE             0000000000000000  00000128

       0000000000000020  0000000000000000   A       0     0     8

  [ 9] .eh_frame         PROGBITS         0000000000000000  00000148

       0000000000000038  0000000000000000   A       0     0     8

  [10] .rela.eh_frame    RELA             0000000000000000  00000440

       0000000000000018  0000000000000018   I      11     9     8

  [11] .symtab           SYMTAB           0000000000000000  00000180

       00000000000001b0  0000000000000018          12    10     8

  [12] .strtab           STRTAB           0000000000000000  00000330

       000000000000004d  0000000000000000           0     0     1

  [13] .shstrtab         STRTAB           0000000000000000  00000458

       0000000000000074  0000000000000000           0     0     1

Key to Flags:

  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),

  L (link order), O (extra OS processing required), G (group), T (TLS),

  C (compressed), x (unknown), o (OS specific), E (exclude),

  l (large), p (processor specific)

 

包含了各个节的起始地址和偏移量等信息

4.3.4重定位节

重定位节的内容如图所示:

重定位节 '.rela.text' at offset 0x380 contains 8 entries:

  偏移量          信息           类型           符号值        符号名称 + 加数

00000000001c  000500000002 R_X86_64_PC32     0000000000000000 .rodata - 4

000000000021  000d00000004 R_X86_64_PLT32    0000000000000000 puts - 4

00000000002b  000e00000004 R_X86_64_PLT32    0000000000000000 exit - 4

000000000054  000500000002 R_X86_64_PC32     0000000000000000 .rodata + 1a

00000000005e  000f00000004 R_X86_64_PLT32    0000000000000000 printf - 4

000000000064  000a00000002 R_X86_64_PC32     0000000000000000 sleepsecs - 4

00000000006b  001000000004 R_X86_64_PLT32    0000000000000000 sleep - 4

00000000007a  001100000004 R_X86_64_PLT32    0000000000000000 getchar - 4

 

重定位节 '.rela.eh_frame' at offset 0x440 contains 1 entry:

  偏移量          信息           类型           符号值        符号名称 + 加数

000000000020  000200000002 R_X86_64_PC32     0000000000000000 .text + 0

可以看到,重定位节中有各种引用的外部符号,给出了他们的偏移量。我们还可以看到重定位节中的外部符号包括了全局变量,以及调用的函数,其中.rodata中的数据是模式串。

有了这些信息之后,在下一步进行链接2,就可以通过重定位节对这些位置的地址进行重定位,使其映射到虚拟内存上。

4.3.5符号表

符号表的内容如下:

Symbol table '.symtab' contains 18 entries:

   Num:    Value          Size Type    Bind   Vis      Ndx Name

     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND

     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS hello.c

     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1

     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3

     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4

     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5

     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    7

     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    8

     8: 0000000000000000     0 SECTION LOCAL  DEFAULT    9

     9: 0000000000000000     0 SECTION LOCAL  DEFAULT    6

    10: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    3 sleepsecs

    11: 0000000000000000   133 FUNC    GLOBAL DEFAULT    1 main

    12: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _GLOBAL_OFFSET_TABLE_

    13: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND puts

    14: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND exit

    15: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND printf

    16: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND sleep

    17: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND getchar

 

No version information found in this file.

可以看到,其中包括了引用的函数,全局变量等等

4.4 Hello.o的结果解析

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

4.4.1 objdump操作

输入的指令如下:

生成的文件如下:

得到的反汇编代码如下:

4.4.2汇编代码比较

对两份汇编代码进行对比,我们可以发现以下的不同之处:

立即数进制不同

Hello.s中的操作数采用10进制编码,而反汇编文件中的操作数都已16进制编码

跳转标识不同

Hello.s中的跳转采用跳转到.L2等方式进行跳转,而反汇编文件中则是采用直接跳转,跳转位置为主函数起始地址加上偏移量。

函数调用机制不同

Hello.s中的函数调用采用直接跳转到函数名的方式进行跳转,而反汇编文件中则是采用直接跳转,跳转位置为主函数起始地址加上偏移量。

4.5 本章小结

这一章中我们主要了解了关于汇编的基础流程和作用,了解到了一个汇编语言文件是如何通过汇编器翻译成机器可识别的机器语言的,以及对于直接编译hello.c文件得到的汇编代码和反汇编hello.o文件得到的汇编代码的区别。对于其中的引用符号的部分全部使用0来填充的问题,也为下一章中的链接和符号重定向做出了铺垫。

(第41分)

 

5章 链接

5.1 链接的概念与作用

5.1.1链接器的概念

链接就是将前一步得到的各种不同文件的代码段和数据段重新整合成两个片段,并且完成相应的符号解析和重定位的,将其组织成一个可执行文件的过程。

5.1.2链接器的作用

将目标文件集合整合成一个文件,并将其映射到对应的虚拟内存,得到可执行文件,并且在链接的过程中,会合并一些提前预制编译的模块,比如printf.o,这也使得程序的运行效率提升(省去了编译printf模块的环节)

5.2 在Ubuntu下链接的命令

采用的指令如下:

生成的文件如下:

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

采用的指令如下:

生成文件如下:

可以看到ELF的文件头如下所示:

ELF 头:

  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00

  类别:                              ELF64

  数据:                              2 补码,小端序 (little endian)

  Version:                           1 (current)

  OS/ABI:                            UNIX - System V

  ABI 版本:                          0

  类型:                              EXEC (可执行文件)

  系统架构:                          Advanced Micro Devices X86-64

  版本:                              0x1

  入口点地址:               0x4010d0

  程序头起点:          64 (bytes into file)

  Start of section headers:          14200 (bytes into file)

  标志:             0x0

  Size of this header:               64 (bytes)

  Size of program headers:           56 (bytes)

  Number of program headers:         12

  Size of section headers:           64 (bytes)

  Number of section headers:         27

  Section header string table index: 26

可以看到里面的信息只要有系统版本,框架,大小,起止位置,此节大小等信息。

而节头如下:

节头:

  [号] 名称              类型             地址              偏移量

       大小              全体大小          旗标   链接   信息   对齐

  [ 0]                   NULL             0000000000000000  00000000

       0000000000000000  0000000000000000           0     0     0

  [ 1] .interp           PROGBITS         00000000004002e0  000002e0

       000000000000001c  0000000000000000   A       0     0     1

  [ 2] .note.gnu.propert NOTE             0000000000400300  00000300

       0000000000000020  0000000000000000   A       0     0     8

  [ 3] .note.ABI-tag     NOTE             0000000000400320  00000320

       0000000000000020  0000000000000000   A       0     0     4

  [ 4] .hash             HASH             0000000000400340  00000340

       0000000000000034  0000000000000004   A       6     0     8

  [ 5] .gnu.hash         GNU_HASH         0000000000400378  00000378

       000000000000001c  0000000000000000   A       6     0     8

  [ 6] .dynsym           DYNSYM           0000000000400398  00000398

       00000000000000c0  0000000000000018   A       7     1     8

  [ 7] .dynstr           STRTAB           0000000000400458  00000458

       0000000000000057  0000000000000000   A       0     0     1

  [ 8] .gnu.version      VERSYM           00000000004004b0  000004b0

       0000000000000010  0000000000000002   A       6     0     2

  [ 9] .gnu.version_r    VERNEED          00000000004004c0  000004c0

       0000000000000020  0000000000000000   A       7     1     8

  [10] .rela.dyn         RELA             00000000004004e0  000004e0

       0000000000000030  0000000000000018   A       6     0     8

  [11] .rela.plt         RELA             0000000000400510  00000510

       0000000000000078  0000000000000018  AI       6    21     8

  [12] .init             PROGBITS         0000000000401000  00001000

       000000000000001b  0000000000000000  AX       0     0     4

  [13] .plt              PROGBITS         0000000000401020  00001020

       0000000000000060  0000000000000010  AX       0     0     16

  [14] .plt.sec          PROGBITS         0000000000401080  00001080

       0000000000000050  0000000000000010  AX       0     0     16

  [15] .text             PROGBITS         00000000004010d0  000010d0

       0000000000000135  0000000000000000  AX       0     0     16

  [16] .fini             PROGBITS         0000000000401208  00001208

       000000000000000d  0000000000000000  AX       0     0     4

  [17] .rodata           PROGBITS         0000000000402000  00002000

       000000000000002f  0000000000000000   A       0     0     4

  [18] .eh_frame         PROGBITS         0000000000402030  00002030

       00000000000000fc  0000000000000000   A       0     0     8

  [19] .dynamic          DYNAMIC          0000000000403e50  00002e50

       00000000000001a0  0000000000000010  WA       7     0     8

  [20] .got              PROGBITS         0000000000403ff0  00002ff0

       0000000000000010  0000000000000008  WA       0     0     8

  [21] .got.plt          PROGBITS         0000000000404000  00003000

       0000000000000040  0000000000000008  WA       0     0     8

  [22] .data             PROGBITS         0000000000404040  00003040

       0000000000000008  0000000000000000  WA       0     0     4

  [23] .comment          PROGBITS         0000000000000000  00003048

       000000000000002a  0000000000000001  MS       0     0     1

  [24] .symtab           SYMTAB           0000000000000000  00003078

       00000000000004c8  0000000000000018          25    30     8

  [25] .strtab           STRTAB           0000000000000000  00003540

       0000000000000150  0000000000000000           0     0     1

  [26] .shstrtab         STRTAB           0000000000000000  00003690

       00000000000000e1  0000000000000000           0     0     1

可以看到,节头描述了各个节的大小、偏移量和其他属性。链接器链接时,会将各个文件的相同段合并成一个大段,并且根据这个大段的大小以及偏移量重新设置各个符号的地址。

5.4 hello的虚拟地址空间

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

我们可以看到程序头的部分如下:

程序头:

  Type           Offset             VirtAddr           PhysAddr

                 FileSiz            MemSiz              Flags  Align

  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040

                 0x00000000000002a0 0x00000000000002a0  R      0x8

  INTERP         0x00000000000002e0 0x00000000004002e0 0x00000000004002e0

                 0x000000000000001c 0x000000000000001c  R      0x1

      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]

  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000

                 0x0000000000000588 0x0000000000000588  R      0x1000

  LOAD           0x0000000000001000 0x0000000000401000 0x0000000000401000

                 0x0000000000000215 0x0000000000000215  R E    0x1000

  LOAD           0x0000000000002000 0x0000000000402000 0x0000000000402000

                 0x000000000000012c 0x000000000000012c  R      0x1000

  LOAD           0x0000000000002e50 0x0000000000403e50 0x0000000000403e50

                 0x00000000000001f8 0x00000000000001f8  RW     0x1000

  DYNAMIC        0x0000000000002e50 0x0000000000403e50 0x0000000000403e50

                 0x00000000000001a0 0x00000000000001a0  RW     0x8

  NOTE           0x0000000000000300 0x0000000000400300 0x0000000000400300

                 0x0000000000000020 0x0000000000000020  R      0x8

  NOTE           0x0000000000000320 0x0000000000400320 0x0000000000400320

                 0x0000000000000020 0x0000000000000020  R      0x4

  GNU_PROPERTY   0x0000000000000300 0x0000000000400300 0x0000000000400300

                 0x0000000000000020 0x0000000000000020  R      0x8

  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000

                 0x0000000000000000 0x0000000000000000  RW     0x10

  GNU_RELRO      0x0000000000002e50 0x0000000000403e50 0x0000000000403e50

                 0x00000000000001b0 0x00000000000001b0  R      0x1

可以看到,程序头包含了PHDR,INTERP,LOAD ,DYNAMIC,NOTE ,GNU_STACK,GNU_RELRO几个部分。

其中PHDR部分负责保存程序头表;INTERP指定了在程序映射到内存之后必须调用的解释器;LOAD则表示一个从二进制文件映射到虚拟地址空间的段,其中保存了常量数据、程序的目标代码等等数据;DYNAMIC保存了由动态链接器使用的信息;而NOTE保存辅助信息;GNU_STACK是其中的权限标志,用于标识栈是否是可执行的;GNU_RELRO则是在指定在重定位结束之后哪些内存区域需要设置为只读区域。

5.5 链接的重定位过程分析

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

输入指令如下:

生成文件如下:

5.5.1hello与hello.o的反汇编文件的不同

反汇编文件中新增了节

    在hello的反汇编文件中新增了.init和.plt节,和一些节中定义的函数。

文件中包含的函数不同

在hello.c中调用的一些库函数被链接器链接到了hello文件中,通过反汇编代码就可以查看到这些新增函数,如exit、printf、sleep、getchar等函数。

重定位之后的函数调用地址不同:

Hello的跳转和函数地址都变成了虚拟内存地址,这就是链接器的重定位的功能。而在hello.o的反汇编代码中,并没有相应的虚拟地址,因此在.rela.text节中为其添加了重定位条目。

访问地址的不同

hello.o中的代码段的起始地址全部为0,需要将其映射到对应的可执行文件的虚拟地址中,因此需要重定位,并且添加重定位条目。

 

5.6 hello的执行流程

可以使用gdb进行逐步调试,得到每一步的地址值:

401000 <_init>

401020 <.plt>

401080 <puts@plt>

401090 <printf@plt>

4010a0 <getchar@plt>

4010b0 <exit@plt>

4010c0 <sleep@plt>

4010d0 <_start>

401100 <_dl_relocate_static_pie>

401105 <main>

401190 <__libc_csu_init>

401200 <__libc_csu_fini>

401208 <_fini>

5.7 Hello的动态链接分析

我们可以在ELF文件中发现如下条目:

[20] .got              PROGBITS         0000000000403ff0  00002ff0

       0000000000000010  0000000000000008  WA       0     0     8

  [21] .got.plt          PROGBITS         0000000000404000  00003000

如果有一个变量,我们利用代码段和数据段的相对位置不变的原则计算正确地址;而如果我们有一个库函数的话,就需要通过plt、got的合作,在plt中初始存的是一批代码,它们跳转到got所指示的位置,然后调用链接器。初始时got里面存的都是plt的第二条指令,随后链接器修改got。这样,在我们的下一次再调用plt时,指向的就是正确的内存地址。

5.8 本章小结

在本章中,我们主要更了解了在Linu系统中链接的运行机制。通过将目标文件hello.o链接为hello可执行文件,我们更加了解了关于链接器的工作细节。并且,通过Ereadelf指令得到了ELF文件,以及通过objdump指令得到了反汇编文件,通过与前几章中的反汇编文件进行比较,我们也更加清楚了关于链接和重定位的具体工作,以及动态链接的具体工作内容。

(第51分)

 

 

6章 hello进程管理

6.1 进程的概念与作用

6.1.1进程的概念

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

6.1.2进程的作用

进程给应用程序提供一种假象:每个进程在运行时,就像是一个独立的逻辑控制流一样,独占整个CPU和内存以及其它资源。

实际上在系统中可能同时运行多个进程,CPU通过快速的处理和进程切换形成并发流,造成看似同时运行的假象。为了实现这样的抽象,系统通过shell程序的合理设计以及上下文切换机制等设计来实现这样的目标。

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

一开始,Shell程序会根据空格符解析输入的命令行,将其拆分为单个单词组成的字符串数组。

接着shell程序会解析得到了结果,分析这是否是一个合法命令,如果不是合法命令则清空输入缓冲区,然后向屏幕输出错误信息或者帮助信息。如果是一个合法命令那么程序会接着判断这是一个内嵌命令(built-in command)或者是一个打开可执行程序的命令,如果是一个内嵌命令那么程序会立即执行这个命令,然后清空输入缓冲区,来到下一个循环,等待新的命令输入。

如果命令行的指令是调用一个程序,那么shell程序会调用fork函数创建1个子进程,并且在子进程中调用execve函数加载输入的程序,如果程序能够正常打开就不会返回,知道程序运行结束,如果程序打开失败,execve函数才会返回。

在加载了程序之后,父进程shell会根据输入的命令行判断该程序是在前台执行还是后台执行,如果是在前台执行的话shell就会等待子进程的结束,然后将其回收,接着等待下一次命令行的输入。如果是在后台运行,那么shell程序就会立即开始等待下一次命令行的输入,不再管当前子进程,知道子进程暂定或是终结,父进程shell程序收到信号,才会对子进程进行回收。

6.3 Hello的fork进程创建过程

根据在6.2节中叙述的流程,我们可以知道,在输入命令./hello后,父进程shell会先解析命令,判断这是一个非内嵌的前台执行的命令,因此父进程调用fork函数,在创建的子进程中,虚拟空间的内容与父进程完全相同,还包括了相同的打开文件:这意味着父进程打开的文件,子进程拥有完全一样的读写权限。二者的不同之处在于其虚拟内存空间指向的物理内存空间不同,而且其进程ID,也就是PID也不同。此外,fork函数被调用一次,但是会分别在父子进程中各返回一次,共计两次,父进程返回子进程的PID,子进程返回0。

6.4 Hello的execve过程

在前文提到的shell父进程创建的子进程中,子进程会调用execve函数,其参数主要是两个二级指针char **argv , char **envp,其作用主要是给出加载程序的参数。只有在加载文件出现错误的时候,例如找不到目标文件,execve函数才会返回,否则就直接执行程序,不再返回。

 

在execve加载了Hello之后,会为新的程序映射虚拟内存空间,并且将控制流转移到新程序的主函数中,主函数main有三个参数int argc , char **argv , char **envp,其中argv和envp分别等于传递向execve函数的参数,argc的数值等于argv指向的字符串数组的元素个数。

此时子进程还会为新的程序映射新的数据段和代码段区域,以及共享区,然后将程序计数器(PC)的值修改为新程序的代码区域入口处的地址。

6.5 Hello的进程执行

6.5.1逻辑控制流

所谓逻辑控制流,就是控制程序的逻辑行为,一步一步的流向,可以使用流程图来表现这种行为的流动。控制流一般分成正常控制流和异常控制流,正常就是一切按预期的方向发展,异常就是控制流的突变

6.5.3上下文

上下文就是内核重新启动一个被抢占的进程所需要恢复的原来的状态,由寄存器、程序计数器、用户栈、内核栈和内核数据结构等构成。此外, 还包括进程打开的文件描述符等等。

6.5.4调度的过程

在系统中,用户进程数一般都多于处理机数、这将导致它们互相争夺处理机。另外,系统进程也同样需要使用处理机。这就要求进程调度程序按一定的策略,动态地把处理机分配给处于就绪队列中的某一个进程,以使之执行。

在进程执行的某些时刻,内核可以决定先停止当前进程的运行,然后重新启动一个先前以及暂停了的进程,这种决策就叫做进程的调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行,我们说内核调度了这个进程。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。

进程之间的调度示意图具体如下:

6.5.5用户态与核心态转换

为了能让处理器安全运行,以不至于使得当前进程修改一些操作系统或者是内核的核心代码和数据,从而造成操作系统的损坏,我们需要对于程序对于不同区域的读写权限进行划分。我们根据权限的不同将模式划分为用户模式与核心模式,核心模式拥有最高的访问权限,何以访问和修改任意处的数据,处理器用一个寄存器来记录当前的模式。对于一个进程,只有在陷入故障,中断或者是系统调用等情况的时候才会进入内核模式,其他时候都始终处于用户模式之下,无法对系统或者是内核的核心数据和代码作出修改,从而保证了操作系统的安全性。

6.6 hello的异常与信号处理

6.6.1异常的类型和处理流程

课本上主要总结了4种类型的异常和相应的处理流程:

中断

中断类型的异常来源于I/O设备的信号,是一种异步的异常,具体的处理流程如下:

可以看到其总是返回下一条指令。

陷阱

陷阱是一种进程故意达成的状态,目的是为了进行系统调动,进入内核模式,其异常是同步的,具体处理流程如下:

可以看到其总是返回下一条指令。

故障

产生故障的原因是因为当前进程发生了一些错误,而这种错误有可能可以被修复(例如缺页错误),也有可能不能被修复。这种异常是同步的,具体处理流程如下:

可以看到,处理程序要么处理故障成功,返回当前导致故障的指令使其重新执行;要么处理故障失败,程序报错并且终止。

终止

程序终止的原因是因为发生了一个不可以被修复的错误,这种异常是同步的,且不会返回,具体流程如下:

 

6.6.2正常运行状态

我们输入运行命令,并且按照要求输入参数,观察程序正常运行的结果如下:

可以看到,程序按照设定的间隔休眠,然后输出指定信息。

6.6.3按下Ctrl+Z:

根据课本上关于信号和进程的知识,我们可以知道,按下Ctrl+Z之后,进程会收到一个SIGSTP 信号,使得当前的hello进程被挂起。用ps指令查看其进程PID,可以发现hello的PID是5763;再用jobs查看此时hello的后台 job号是1,调用指令fg 1将其调回前台。

6.6.4按下Ctrl+C

根据课本上的相关知识,我们知道此时进程收到一个SIGINT 信号,一次结束 hello。我们输入ps指令,发现查询不到hello进程的PID,输入指令jobs,发现也没有对应作业,因此hello进程被彻底终止。

6.6.5中途乱按:

此时的程序只会将其记录在输入缓冲区,不会影响程序的运行,但是如果在hello运行结束后还有乱按的指令在缓冲区的,那么就会被作为新的命令行输入。

6.6.6Kill命令:

通过kill指令向所在的挂起的进程发出终止指令,在此之后,通过ps指令无法找到对应的进程,对应的jobs指令也无法找到作业,说明进程已经被终止。

6.7本章小结

我们在本章进一步地巩固了关于hello作为进程在运行的时候的一些相关的知识。主要涉及到在shell程序中hello进程是如何被创建、加载和结束的,以及通过键盘输入的指令是如何在输入缓冲区等待和清除的。以及关于在hello进程执行的过程中,是如何捕获信号并且对信号作出相应的反应和行为的。对于不同的信号,有着不同的处理方式和结果。我还收获了关于如何在shell进程中查看进程和作业,如何设置前台或者是后台作业等等。

(第61分)

 

7章 hello的存储管理

7.1 hello的存储器地址空间

7.1.1逻辑地址

逻辑地址是指在计算机体系结构中是指应用程序角度看到的内存单元(memory cell)、存储单元(storage element)、网络主机(network host)的地址。 是指由程序产生的与段相关的偏移地址部分。

7.1.2线性地址

线性地址(Linear Address)是逻辑地址物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。

7.1.3虚拟地址

虚拟地址是程序运行在保护模式下,这样程序访问存储器所使用的逻辑地址称为虚拟地址。

7.1.4物理地址

 放在寻址总线上的地址。放在寻址总线上,如果是读,电路根据这个地址每位的值就将相应地址的物理内存中的数据放到数据总线中传输。如果是写,电路根据这个地址每位的值就在相应地址的物理内存中放入数据总线上的内容。物理内存是以字节(8位)为单位编址的。

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

一个逻辑地址的构成有两个部分:分别是段标识符和段内偏移量。其中,段标识符是一个由16位长数据组成的字段,将其称为段选择符。其中的前13位是一个索引号。而后面三位中则包含了一些硬件的细节。

段式内存管理方式就是直接将逻辑地址转换成物理地址,也就是CPU不支持分页机制。其地址的基本组成方式是段号+段内偏移地址。

在x86保护模式下,段的信息即段描述符占8个字节,段信息无法直接存放在段寄存器中。Intel的设计是段描述符集中存放在GDT或LDT中,而段寄存器存放的是段描述符在GDT或LDT内的索引值。

首先给定一个完整的逻辑地址,其次,看段选择描述符中的T1字段是0还是1,可以知道当前要转换的是GDT中的段,还是LDT中的段,再根据指定的相应的寄存器,得到其地址和大小,我们就有了一个数组了。接着,拿出段选择符中的前13位,可以在这个数组中查找到对应的段描述符,这样就有了Base,即基地址就知道了。最后,把基地址Base+Offset,就是要转换的下一个阶段的地址。

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

首先,将线性地址划分为VPN+VPO的格式,然后将VPN拆分为TLBT+TLBI的格式,然后在TLB中寻找对应的PPN,如果有缺页的情况发生,那么就去下一级页表中寻找对应的PPN,以此类推。找到PPN知乎,将其与之前的VPO进行组合就得到了对应的物理地址。

 

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

多级页表将虚拟地址的VPN划分为相等大小的不同的部分,每个部分用于寻找由上一级确定的页表基址对应的页表条目。其结构类似于多级缓存的结构。

在四级页表的情况下,会将VPN的部分分成等长的4个段,将每一个段作为某一级页表的索引,直到找到正确的PPN。

多级页表示意图如下:

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

根据上述步骤得到了一个物理地址PA之后,根据Cache的具体结构,我们将PA划分为CT(标记位),CS(组号)和CO(偏移量)

接下来,我们根据CS寻找到正确的组,比较每一个缓存行是否标记位有效以及CT是否相等。如果命中,就返回CO偏移量指向的数据,如果没有命中,就依次去L2,L3,主存判断是否命中,当命中时,将数据传给CPU同时更新各级cache的缓存行。

多级Cache示意图如下:

7.6 hello进程fork时的内存映射

当fork函数被当前进程调用时,首先,内核会为新进程创建各种与父进程相同的数据结构,并分配给它一个唯一的PID,同时为这个新进程创建虚拟内存。

当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任意一个在之后进行写操作时,写时复制机制就会为其创建新页面。因此,也就为每个进程保持了私有空间地址的抽象概念。

7.7 hello进程execve时的内存映射

在exceve函数加载和执行hello程序的过程中,需要执行几个步骤:首先删除已存在的用户区域。然后,会映射私有区域,该函数会为Hello的代码、数据、bss和栈区域,并创建新的区域结构,这些区域都是私有、写时复制的。接着,.映射共享区域,比如,在Hello程序与标准C库libc.so链接,这些对象都是动态链接到Hello的。最后再设置程序计数器,完成相应的跳转。

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

如果在页表中发生了一次不命中,我们就认为发生了一次缺页故障,具体处理流程如下:

首先,由于PTE中对应页的有效位是0,所以MMU出发了一次缺页异常信号,此时操作系统就开始调用相应的异常处理程序。被调用的缺页处理程序会根据一定的替换策略确认出物理内存中的牺牲页,如果这个页已经被修改了,程序就会把它换到磁盘。缺页处理程序在物理内存中写入新的页面,并更新内存中的PTE。完成了上述操作之后,缺页处理程序就会返回到原来的进程,再次执行刚才导致缺页的命令。此时,因为虚拟页面已经换存在物理内存中,所以就会命中。

7.9动态存储分配管理

动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存空间,其要么是已分配的,要么是空闲的。动态内存分配主要有两种基本方法与策略:

隐式空闲链表

隐式空闲链表通常带有一个头部和脚部标签,内容相同,记录块的大小和是否分配等信息。一般来说每个块都是由头部和脚部、有效载荷、可能的额外填充组成,对于某些优化的链表,已分配的块可以没有脚部。

在隐式空闲链表中,所有的块都是通过头部和脚部中的大小字段连接着的。因此分配器可以依次遍历整个堆。其中,一个设置了已分配的位而大小为零的终止头部将作为特殊标记的结束块。

当一个应用请求一个k字节的块时,分配器搜索空闲链表,查找一个足够大的可以放置所请求块的空闲块。分配器有三种放置策略:首次适配、下一次适配合最佳适配。分配完后可以分割空闲块减少内部碎片。同时分配器在面对释放一个已分配块时,可以合并空闲块,其中便利用隐式空闲链表的边界标记来进行合并。

显示空间链表管理

显式空闲链表是将空闲块组织为某种形式的显式数据结构。堆被组织成一个双向链表,在每个空闲块中,都包含一个前驱与一个后继指针。

在显式空闲链表中,可以采用后进先出的顺序维护链表,将最新释放的块放置在链表的开始处,也可以采用按照地址顺序来维护链表。其中链表中每个块的地址都小于它的后继地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。

7.10本章小结

在本章中我们主要了解了关于虚拟内存的基本思想和基本机制,以及如何通过虚拟地址得到一个物理地址,如何通过TLB机制来加速寻找的过程。我们还了解了关于堆的动态分配的不同机制,关于空闲块管理和分配的机制的不同我们可以得到不同的效果的堆分配器。还了解了一个新的进程在被创建或者是一个程序被加载时,系统如何为其分配和创建存储空间,以及私有文件和共享文件在存储和读写时候的不同机制。

(第7 2分)

 

8章 hello的IO管理

8.1 Linux的IO设备管理方法

8.1.1设备的模型化

我们可以将所有的I/O设备都被模型化为文件,方便统一进行操作。

8.1.2设备管理

这种将设备统一模型化为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。这样,我们就可以简单地对所有的文件执行open/close,read/write或是lseek操作等等。

8.2 简述Unix IO接口及其函数

8.2.1Unix IO接口

我们可以通过接口提供的方式进行打开文件操作,在打开一个特定的文件的时候,内核返回一个非负整数的文件描述符(fd),用于代表当前打开的文件。Linux shell创建的每个进程开始时都有三个打开的文件:标准输入(fd=0),标准输出(fd=1)和标准错误(fd=2)。每当打开一个新的文件时,文件描述符fd总是选用当前未使用的最小非负整数。

每一个进程都可以通过调用fseek函数设置文件的当前位置为k。

在进行读写文件时,读操作时将从文件的k处开始的n个字节复制到内存,然后将源文件的位置从k移到k+n;写操作是从内存指定位置复制n个字节到文件位置为k的地方,然后重置目标文件的位置为k

关闭文件:当应用完成对文件的访问后,通知内核关闭这个文件。内核会释放文件打开时创建的数据结构,将文件的描述符置为未使用。

8.2.2Unix IO函数

open函数

此函数用于打开或创建文件,在打开或创建文件时可以指定文件的属性及用户的权限等各种参数。如果打开成功就返回其文件描述符,如果失败则返回-1

close函数

该函数用于关闭一个被打开的的文件,该文件通过输入参数文件描述符fd确定,如果成功关闭则返回0,如果关闭失败则返回-1。

read函数

该函数带有缓冲区,用于从文件读取数据。返回读取到的文件字节数,如果直接读取到EOF就返回0,读取失败返回-1。

write函数

该函数用于向文件写入数据,如果写入成功就返回向其写入的字节数,写入失败就返回-1。

lseek函数

该函数用于将文件描述符指定的文件的指针位置移到别处中,如果成功则返回当前位置,失败就返回-1。

8.3 printf的实现分析

首先,我们给出printf函数的代码如下:

int printf(const char *fmt, ...){

int i;

       va_list arg = (va_list)((char *)(&fmt) + 4);

       i = vsprintf(buf, fmt, arg);

       write(buf, i);

       return i;

}

其中引用的vsprintf函数代码如下:

int vsprintf(char *buf, const char *fmt, va_list args){

char *p;

chartmp[256];

   va_listp_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函数的作用是将所有的参数内容格式化之后存入buf,然后返回格式化数组的长度
    write函数是将buf中的i个元素写到终端的函数。

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

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

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

8.4 getchar的实现分析

getchar有一个int型的返回值。当程序调用getchar时,程序就等着用户按键,用户输入的字符被存放在键盘缓冲区中直到用户按回车为止(回车字符也放在缓冲区中)。

当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的第一个字符的ascii码,如出错返回-1,且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为后,才等待用户按键。

8.5本章小结

本章主要介绍了Linux的I/O设备的抽象处理和读取写入等操作,我更加深入地了解了关于文件描述符以及文件打开,文件位置等机制,以及关于子进程复制一份父进程的文件表等机制以及相关的用途和特性等等。

(第81分)

结论

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

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

(结论0分,缺失 -1分,根据内容酌情加分)

 

附件

文件名

文件作用

hello.i

预处理后的ASCII码文件

hello.s

编译之后得到的文件

hello.o

汇编之后得到的目标文件

hello

链接之后得到的可执行文件

hello_disassemble.s

目标文件hello.o文件反汇编得到的结果

hello_objdumo.s

可执行文件hello反汇编之后的结果

helloELF.txt

目标文件hello.oELF格式

helloELF1.txt

可执行文件hello的ELF格式

 

 

(附件0分,缺失 -1分)

 

参考文献

[1]刘磊,熊小鹏.最小驻留价值缓存替换算法[J].计算机应用,2013,33(04):1018-1022.

[2]眭俊华,刘慧娜,王建鑫,秦庆旺.多核多线程技术综述[J].计算机应用,2013,33(S1):239-242+261.

[3]贺伟.malloc函数在Linux系统下的原理性实现[J].福建电脑,2010,26(06):154-155.

[4]陆金江.深入探究C中的malloc()和free()函数[J].计算机光盘软件与应用,2013,16(21):120+122.

[5]韩静,彭双和,赵佳利.动态内存分配及内存泄漏相关概念的案例教学[J].计算机教育,2019(01):111-115.

[6]黄毅伟. TLB的设计与验证[D].国防科学技术大学,2006.

[7]王超宇. 缓存替换策略研究[D].哈尔滨工程大学,2012.

(参考文献0分,缺失 -1分)

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值