CSAPP 计算机大作业 hello的一生

2018-12-31  本文已影响0人  icey_J

摘 要

为了对计算机系统有着更深入的了解以及研究系统间的协作关系。本大作业针对hello程序运行的一系列过程对c文件的预处理、编译、汇编、链接,Linux操作系统的进程管理,Linux虚拟内存管理以及硬件上的内存访问,LinuxIO管理进行研究。本大作业的实验环境为Ubuntu 18.04,gcc编译器,gdb,objdump反汇编工具。本大作业对于以后研究计算机系统相关领域有着铺垫和引导作用。

关键词:计算机系统;编译;虚拟内存;IO管理;进程管理

目 录

第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):

在notepad或IDE中将hello的代码输入保存为c格式的文件,形成hello.c文件。这就是program,程序。在OS(例如Linux)中,通过交互式应用程序shell上,输入命令行对c文件进行编译,通过cpp(预处理器)预处理,ccl(编译器)编译,as(汇编器)汇编,最后通过ld(链接器)链接生成hello可执行目标程序,接着操作系统将fork()的子进程通过excve()函数将hello程序载入并创建运行环境,例如mmap将hello.c所编译处的可执行文件映射到虚拟内存中,操作系统分配cpu的时间片给hello进程。

这样一个hello的进程(process)就诞生了。以上就是P2P,从程序到进程的过程。

020(from zero-0 to zero-0):

在上述的P2P过程完成后,hello进程通过操作系统的调用获得cpu的控制权,并且在操作系统的高层抽象下,硬件I/O设备和其他资源被抽象为文件供hello进程使用(Linux下)。每当hello程序需要数据时,cpu会执行读取某一虚拟地址的命令,通过MMU将va翻译为pa,再去缓存或内存中读取。在程序使用完毕后,无论通过自身调用exit()终止函数还是通过外部的键盘发送终止信号又或者因为异常保护被操作系统终止退出,信号处理函数都会接受hello进程结束的SIGCHLD信号或者SIGINT信号并执行回收处理。操作系统或者其父进程shell会回收子进程并释放内存空间,hello进程重归于零。

hello程序从无到有,hello进程从执行到结束,这就是020,从零到零的过程。

1.2 环境与工具

列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。

硬件环境:

CPU:Intel Core i5 6300HQ;2.3GHz;8G RAM;128G SSD;1T HDD

软件环境:

OS:

windows 10 64bit;Ubuntu 18.04 LTS 64bit

开发调试工具:

GDB;EDB;OBJDUMP;READELF;vim;gcc

1.3 中间结果

列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。

hello.i:hello.c文件预编译后的文件,用于预编译章节说明和用于汇编汇编文件。

hello.s:hello.i经过编译器编译后的文件,用于汇编章节说明以及用于汇编器汇编可重定位文件。

hello.o:hello.s经过汇编器汇编后的文件,为16进制代码文件,用于链接器链接生成可执行文件。

hello:hello.o链接后生成的可执行目标文件,用来进行反汇编研究汇编代码或者用来运行。

hello:同hello.out文件,后缀不为.out,用于执行.

1.4 本章小结

本章对hello程序的运行的整个过程进行了简单的介绍,以及介绍了此次大作业所使用的相关软硬件工具,最后介绍了大作业所生成的中间文件以及作用.

第2章 预处理

2.1 预处理的概念与作用

2.1.1预处理的概念

预处理是指,预处理阶段,预处理器(cpp)根据以字符#开头的命令,修改原始的C程序.预处理指令为#开头的命令行,包含条件编译(#if,#ifdef),宏定义(#define),源文件包含(#include),行控制(#line),错误指令(#error)等.

2.1.2预处理的作用

用以支持特定的语言特性,对代码进行替换,以便接下来对文件的编译.

2.2在Ubuntu下预处理的命令

2.2.1预处理的相关命令

Ubuntu下预处理的命令为:

$ gcc -E hello.c -o hello.i

或者

$ cpp hello.c hello.i

2.2.2预处理过程演示

image003.png

图2-1


image005.png

图2-2

2.3 Hello的预处理结果解析

2.3.1预处理结果演示

下面是部分预处理后的代码截图,例如开头(图2-3),调用外部函数(图2-4),对一些变量进行重新定义命名(图2-5)以及最后的源代码部分(图2-6)

image006.png

图2-3

![![image012.png](https://img.haomeiwen.com/i8130705/a0500aa055409214.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ](https://img.haomeiwen.com/i8130705/f1a3d314bb68b484.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

图2-4

image010.png

图2-5

image012.png

图2-6

2.3.2预处理结果解析

从hello.i文件中可以看到,预处理对代码中#开头的命令进行替换,将所引用的头文件中的函数进行引入.将hello文件扩充为一个完整的代码文件.

2.4 本章小结

本章介绍了预处理的相关概念和作用.并在Ubuntu中进行了实际演示,并且对生成的.i文件进行了解析.预处理是对#开头的命令进行替换,将完整的代码引入. 预处理指令为#开头的命令行,包含条件编译(#if,#ifdef),宏定义(#define),源文件包含(#include),行控制(#line),错误指令(#error)等.

第3章 编译

3.1 编译的概念与作用

3.1.1 编译的概念

编译指从.i文件经过编译器处理后产生.s文件的过程.其中包括五个阶段:

词法分析,语法分析,语义检查,中间代码生成,目标代码生成。

3. 1. 2 编译的作用

编译将.i文件编译为.s汇编代码文件,用于接下来汇编器将其处理为可重定位目标文件,以进行接下来的链接等操作。当然,也可以用来进行汇编代码的分析。

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

3.1 在Ubuntu下编译的命令

3.2.1 编译的相关命令

Ubuntu下编译命令为:

$ gcc -S hello.i -o hello.s

或者

$ cc1 hello.i -Og hello.s

3.1.2 编译过程的演示

![image016.png](https://img.haomeiwen.com/i8130705/e25fac274acd9f93.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

图3-1 编译命令


image016.png

图3-2 编译结果

3.1 Hello的编译结果解析

image017.png
image019.png

图3-3 hello.s代码

汇编文件中几个指令解释

|

指令

|

解释

|
|

.file

|

声明源文件

|
|

.text

|

以下是代码描述

|
|

.globl

|

表明后面所跟为全局变量

|
|

.data

|

以下是数据描述

|
|

.align

|

表明对齐方式

|
|

.type

|

表明对象是对象还是函数

|
|

.size

|

表明大小

|
|

.long

|

未知

|
|

.section

|

表明数据所在节

|
|

.string

|

表明为字符串,后跟字符串内容

|
|

.ident

|

表明所用编译器版本

|

表3-1 指令含义

3.3.1 常量

hello文件中所用到的常量为字符串,hello.c中一共有两个字符串常量:

1. "Usage: Hello 学号 姓名!\n"

2. "Hello %s %s\n"

汇编文件中关于这两个字符串的描述中,开头为.LC0和.LC1,其中一个汉字对应三个\xxx,所以hello后面跟的是汉字“学号”“姓名”以及“!”,常量存放在.rodata节,为只读数据。

image021.png

图3-4 字符串相关指令

3.3.2 变量

1. 全局变量

全局变量作为程序的公共变量,可以为程序中所有函数所用。hello文件中包含一个全局变量sleepsecs。在代码中可以看到sleepsecs被声明为全局变量,在数据描述上,对齐方式为4字节,并且为对象,sleepsecs大小为4个字节

图3-5 sleepsecs相关指令

2. 局部变量

局部变量相对于全局变量,位于函数内部,在使用时被创建,在函数结束后被销毁。hello文件中的局部变量为计数变量i。由于i是局部变量,所以编译器会将i的值存储在栈中或者寄存器中。hello.s中局部变量i被编译器存放在了栈中,位置为-4(%rbp),在.L2节中,i被初始化为0,随后进入.L3节中进行循环。

image025.png

图3-6 局部变量i的存放

3.3.3 表达式、类型

1. 表达式

C语言中全局变量是表达式,一条语句也是表达式,编译器将一条表达式拆分为多条汇编语句的组合来表达表达式。例如图3-8就是语句

printf("Hello %s %s\n",argv[1],argv[2]);

的汇编代码。

2. 类型

这里的类型理解为数据类型,hello.c中出现了这几种数据类型:

整型、字符数组(字符串)

   编译器将全局变量整型存储在只读数据中,调用时存入寄存器,字符数组的地址存放在栈中。如图3-5,3-6.

3.3.4 赋值操作

hello.c中出现的赋值操作为为全局变量sleepsecs赋值时,变量i初始化时,编译器对两种赋值有不同的编译处理。

在对全局变量sleepsecs赋值时,编译器直接通过指令将值设为2

[图片上传失败...(image-ba1d8f-1546271099965)]

图3-10 sleepsecs赋值

在对i初始化赋值时,通过movl指令将立即数$0传给i

[图片上传失败...(image-64670d-1546271099965)]

图3-11 i初始化

3.3.5 赋初值/不赋初值

在hello.c中main函数的局部变量i没有赋初值,同时全局变量sleepsecs初始化时即赋初值,关于sleepsecs不再赘述。建立变量i的语句出现在判断argc之前,而在编译后有关判断argc代码之前没有出现任何关于i的语句,但是实际上编译器已经为i申请了栈中存储的位置-4(%rbp),也就是说,对于不赋初值的局部变量,编译器会提前申请栈中空间。而全局未初始化变量则在.bss节中不处理。

[图片上传失败...(image-39b981-1546271099964)]

图3-12 源码

i103j�َc<�
3.3.9 控制转移

文件中出现的控制转移为if(),for()结构分别解析

  1. if条件分支结构

编译器中通过jmp语句实现分支结构的跳转(或者cmoveX),通过cmp进行判断。hello.c中出现的条件分支结构为

[图片上传失败...(image-79308f-1546768413784)]

图3-17 if结构

编译器将将其解释为汇编代码

[图片上传失败...(image-331aa8-1546768413784)]

图3-18 if结构对应汇编

  1. for循环结构

编译器对于for循环结构的处理逻辑比较像dowhile结构,首先会跳转到底部的i的比较代码,然后比较后才正式开始循环。hello.c中for循环结构为

[图片上传失败...(image-a33f38-1546768413784)]

图3-19 for结构

可以看到编译器首先跳转到L2进行i的初始化,接着直接跳转到L3进行比较,小于等于9则跳回L4进行正式的循环

[图片上传失败...(image-93e98e-1546768413784)]

图3-20 for汇编

3.3.10 函数操作

  1. 参数传递(地址/值)

函数参数属于形参, hello.c文件中main函数传入时使用了参数,在调用printf中,也使用了参数,sleep函数中也传入了sleepsecs参数,具体来说,参数不属于变量,这里描述参数传递时的汇编代码形式。

如3-7图所示,寄存器%edi保存argc参数,%rsi保存argv[]数组的指针。通过movl(双字传送)和movq(四字传送)指令传值,这里是因为argc为4字节而数组指针为8字节,也分别对应了值和地址的传送。

[图片上传失败...(image-c390f-1546768413784)]

图3-7 main函数参数

如图3-8,printf函数传入的三个参数分别保存在了%rdi,%rsi,%rdx中,%rdi中传入.LC1,也就是格式串,argv参数列表分别通过两次内存引用取到参数1,2地址传入寄存器。

[图片上传失败...(image-975992-1546768413784)]

图3-8 printf函数参数

sleep函数的参数通过%edi传入。其中由于sleepsecs是全局变量,最后链接后的汇编代码会通过相对地址偏移在.data节中读值,%rip指代的就是存储地址(可能)

[图片上传失败...(image-148152-1546768413784)]

图3-9 sleep函数参数

  1. 函数调用

这里要先说明一下通用栈帧的结构,在调用函数时,调用者通常会将调用完毕后的返回地址压入栈中,在栈的返回地址之前,调用者还有可能会保存一些参数用来传递。在返回地址之后,被调用者在栈中开辟新的空间使用,从返回地址为止,所有调用者在栈中的结构称为“调用者的栈帧”

[图片上传失败...(image-bbf26d-1546768413784)]

图3-21 栈帧结构

hello.c中出现的函数调用有printf函数调用,exit函数调用,sleep函数调用,getchar函数调用。这里需要注意各个寄存器的使用规则,传参用寄存器依次为%rdi,%rsi,%rdx.%rcx,保存返回值的寄存器为%rax。编译器使用call指令对函数进行调用,通过寄存器传值或者栈传值,通过rax读取函数的返回值。

[图片上传失败...(image-c99cd2-1546768413784)]

图3-22 main函数开辟新的栈空间作为栈帧

[图片上传失败...(image-f50619-1546768413783)]

图3-23 exit函数调用

[图片上传失败...(image-80be5-1546768413783)]

图3-24 printf调用和传参

[图片上传失败...(image-f63758-1546768413783)]

图3-25 寄存器规则

  1. 函数返回

编译器使用ret指令进行函数返回,在此之前会用%rax保存函数的返回值,ret指令的作用为:取出栈顶的地址作为跳转目标并进行跳转,这也说明了为什么call指令时会将返回地址压入栈中。

[图片上传失败...(image-a5b8f-1546768413783)]

图3-26 main函数返回代码

3.4 本章小结

本章主要介绍了编译器对于源代码的相关处理的内容。编译器通过不同的汇编代码结构将c语言代码转换为汇编代码,从而方便下面的汇编器和链接器将其实现为最终的可执行目标文件。编译器对于c中的数据和操作有着不同的处理方式,数据包括常量、变量、表达式、宏等,操作包括赋值、转换、算术操作、关系操作、控制转移、函数操作等。

第4章 汇编

4.1 汇编的概念与作用

4.1.1. 汇编的概念

汇编是指汇编器将.s文件翻译为.o文件的过程,汇编器将汇编代码编译为对应的机器指令,并将结果保存在.o文件中。

4.1.2. 汇编的作用

将汇编代码翻译为对应的机器语言,以便能够为链接器所用。

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

4.2 在Ubuntu下汇编的命令

4.2.1. 汇编的相关命令

Ubuntu下的汇编相关命令为:

$ gcc -c hello.s -o hello.o

或者

$ as -o hello.o hello.s

4.2.2. 汇编的过程演示

[图片上传失败...(image-d489c9-1546768413783)]

图4-1 汇编命令

[图片上传失败...(image-997a8f-1546768413783)]

图4-2 生成.o文件

4.3 可重定位目标elf格式

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

|

ELF格式名称

|

相关解释

|
|

ELF头

|

描述生成该文件的系统的字大小和顺序以及ELF头大小、目标文件类型、机器类型、节头部表的偏移、节头部表中条目大小和数量

|
|

.text

|

以编译的机器代码

|
|

.rodata

|

只读数据

|
|

.data

|

已初始化的全局和静态C变量

|
|

.bss

|

未初始化的全局和静态C变量以及初始化为0的全局静态变量

|
|

.symtab

|

符号表,存放代码中定义或引用的函数和全局变量的信息

|
|

.rel.text

|

一个表示.text节中位置的列表,指向的是链接时需要修改位置的代码在text节中的位置

|
|

.rel.data

|

被模块引用的或定义的全局变量的重定位信息

|
|

.debug

|

调试符号表,包含局部/全局变量和C源文件

|
|

.line

|

行号和text节中机器指令的映射

|
|

.strtab

|

字符串表,.symtab和.debug中符号表及节头部中节的名字

|
|

节头部表

|

描述目标文件的节

|

表4-1 ELF文件格式

[图片上传失败...(image-f04dc0-1546768413783)]

图4-3 hello.o的ELF文件头

[图片上传失败...(image-4c1ee6-1546768413783)]

图4-4 hello.o文件的节头信息(名称、类型、地址、偏移量)

[图片上传失败...(image-9a8630-1546768413783)]

图4-5 重定位节

链接器会根据重定位节中的信息对重定位文件进行重定位。

[图片上传失败...(image-a6b765-1546768413783)]

图4-6 .symtab节

重定位项目分析:

.rel.text中每一条重定位条目都包括如下四个内容:

|

名称

|

含义

|
|

offset

|

被修改的引用在节中的偏移

|
|

type(32bit)

|

重定位的方式

|
|

symbol(32bit)

|

被修改的引用应该指向的符号

|
|

addend

|

被修改引用的偏移调整

|

表4-2 ELF重定位条目

图4-5中重定位节中以第一条.rodata为例,实际上这是.LC0的字符串的重定位条目

[图片上传失败...(image-2952a-1546768413783)]

图4-7 L1重定位条目

偏移量就是offset,信息是type和symbol的总和,类型为PC32位相对引用。

由于rela起始于0x338,使用hexedit查看如图4-8,接下来简单说明链接器之后会如何使用重定位条目

[图片上传失败...(image-8fd26a-1546768413783)]

图4-8 hexedit重定位条目

假设LC0重定位条目为r, r.offset = 0x18,r.type = R_X86_64_PC32, r.symbol = .rodata, r.addend = -4

用PC相对地址引用,令.text节中需要修改的位置为s,重定位的目标位置为d,那么

引用的运行位置 refaddr = ADDR(s) + offset;

更新此处的内容 *refptr = (unsigned) (ADDR(s) – refaddr + addend);

上述计算完成后,即完成了关于LC0的重定位内容,printf函数中字符串指向的地址改为了.data节中字符串存放的地址。其余几条同理。

4.4 Hello.o的结果解析

[图片上传失败...(image-4c7e50-1546768413783)]

图4-9 反汇编内容

[图片上传失败...(image-f6be3f-1546768413783)]

[图片上传失败...(image-28a1c6-1546768413783)]

图4-10 hello.s中main函数内容

由图4-11对比图可以看出,两者之间的差别并不是很大,而经过汇编生成的可重定位文件与之前的汇编文件区别主要在于以下四点:

1. 立即数的进制改变:

立即数由原先的10进制改变成了16进制,向机器级代码更近了一步。

2. 分支转移:

汇编文件中的伪代码段地址被替换成了真正的地址(暂时),可以看到凡是jxx指令后跟的Lx都被替换为了十六进制main函数中的地址。

3. 函数调用:

因为调用的都是共享库中的函数,调用函数的call指令由原先的简单指令被替换为了下一条地址,设置为了重定位格式并且指明了重定位类型,等待最终链接器将其确定最终的位置。

4. 全局变量访问:

所有全局变量访问都由伪代码段名称+(%rip)的形式改为了0 + (%rip)形式,并且添加了重定位条目,等待最终链接器的确定。

[图片上传失败...(image-dc062a-1546768413783)]

[图片上传失败...(image-625f5c-1546768413783)]

图4-11 两者对应关系

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

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

4.5 本章小结

本章介绍了程序生成过程中编译器汇编的相关内容。汇编过程将汇编语言转换为机器代码,生成可重定位的目标文件,使机器能够直接处理与执行。通过readelf读取其elf信息与重定位信息,得到相关信息。通过objdump反汇编目标文件,进行对照。

第5章 链接

5.1 链接的概念与作用

5.1.1.链接的概念

链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载到内存执行。链接可以执行与编译时,也就是在源代码被翻译为机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存执行时;甚至执行于运行时,由应用程序来执行。这里的链接指的是.o文件经过链接器处理为可执行文件的过程。

5.1.2.链接的作用

通过链接,将可重定位目标文件中引用的外部函数、数据统一合并到一个文件,处理为完整的可执行目标文件。

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

5.2 在Ubuntu下链接的命令

5.2.1. 链接的相关命令

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.2.2. 链接的过程演示

[图片上传失败...(image-dae0db-1546768413782)]

图5-1 链接命令

[图片上传失败...(image-363549-1546768413782)]

图5-2 生成文件 hello

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

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

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

|

ELF格式名称

|

解释

|
|

ELF头

|

描述文件的总体格式,包括程序的入口点,第一条指令的地址

|
|

段头部表

|

将连续文件节映射到内存段

|
|

.init

|

包含函数_init()初始化代码

|
|

.text

|

链接后的代码段

|
|

.rodata

|

只读数据

|
|

.data

|

已初始化的全局和静态C变量

|
|

.bss

|

未初始化的全局和静态C变量和初始化为0的

|
|

.symtab

|

符号表,显示引用的函数和全局变量符号

|
|

debug

|

调试符号表,包含局部/全局变量和C源文件

|
|

.line

|

行号和text节中机器指令的映射

|
|

.strtab

|

字符串表,.symtab和.debug中符号表及节头部中节的名字

|
|

节头部表

|

描述目标文件的节

|

表5-1 可执行文件的ELF格式

[图片上传失败...(image-be3c9c-1546768413782)]

图5-3 ELF头

: initi��z�X��
节头信息中包含了各段的基本信息,其中包括了各段的起始地址,大小,偏移量以及类型,通过这些可以找到相关的段内容。

[图片上传失败...(image-456635-1546768649790)]

图5-5 程序头

[图片上传失败...(image-fbfe06-1546768649790)]

图5-6 各段的映射以及动态库信息

[图片上传失败...(image-47f7c7-1546768649790)]

图5-7 重定位的信息

[图片上传失败...(image-314bc1-1546768649790)]

[图片上传失败...(image-57435e-1546768649790)]

[图片上传失败...(image-5d2687-1546768649790)]

图5-8 符号表中的信息

5.4 hello的虚拟地址空间

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

举几个节来分析

1. .init节

如图所示,init节的起始的虚拟地址为0x400488,对hello中init节进行反汇编得到_init函数的反汇编代码如图5-10,通过edb的memorydump查看该地址的相关信息(图5-11),可以看见操作码与反汇编一致,即此处为intit节的位置。

[图片上传失败...(image-5d7446-1546768649790)]

图5-9 init起始地址

[图片上传失败...(image-f9f93-1546768649790)]

图5-10 反汇编

[图片上传失败...(image-28fd42-1546768649790)]

图5-11 edb对应地址的内容

2..text节

同init节的方法

[图片上传失败...(image-2aa473-1546768649790)]

图5-12 .text节的起始地址

[图片上传失败...(image-6dfa9a-1546768649790)]

图5-13 _start函数的反汇编

[图片上传失败...(image-a4752c-1546768649790)]

图5-14 text节的起始地址内容

3. plt节

[图片上传失败...(image-e941ce-1546768649790)]

图5-15 plt节起始地址

[图片上传失败...(image-7a9bdc-1546768649790)]

图5-16 .plt函数反汇编

[图片上传失败...(image-60c31a-1546768649790)]

图5-17 edb中地址相关内容

5.5 链接的重定位过程分析

[图片上传失败...(image-118633-1546768649790)]

图5-18 .o反汇编

[图片上传失败...(image-b1ba9e-1546768649790)]

图5-19 可执行文件反汇编(main部分)

hello.o文件反汇编后只有main的汇编代码,因此下面只比较分析main函数部分的变化。

1. 虚拟地址

经过链接器处理后,main中指令的虚拟地址被最终确定,.o文件中main函数指令地址是以0开始,而最终的地址是以0x400532开始,链接器通过将函数中调用的外部函数和全局变量重定位后,分配最终地址。

2. 对于函数和数据的引用

.o文件中的函数和全局变量使用R_X86_64_PC32和R_X86_64_PLT32表明重定位方式而且没有指明具体的函数和变量位置,这是因为在通过链接器处理以前,汇编器并不知道相关数据的位置,而在ld链接器链接其他文件时,动态库中函数加入到其中,其中函数和数据位置可以确定,ld就通过定位方式进行相对偏移量的计算,指向相应函数,关于****hello****如何对hello.o****中的重定位项目进行重定位,具体过程第4****章4.3****有说明,在此不再赘述

3. 函数数量

除了main函数之外,可执行目标文件的反汇编中增加了很多额外的辅助函数,这些函数都来自与所引用的库文件中,比如_start入口函数,_libc_start_main函数用于调用main。这些函数帮助程序正常的运行,为程序提供了入口和退出后的处理流程。

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

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

5.6 hello的执行流程

|

程序

|

地址(0x)

|
|

ld-2.27.so!_dl_start

|

00007f8f6e2aeea0

|
|

ld-2.27.so!_dl_init

|

00007ff99ff450c5

|
|

hello!_start

|

00007ff99fb74ab0

|
|

ibc-2.27.so!__libc_start_main

|

00007fff9c867ab0

|
|

hello!puts@plt

|

000000000040054e

|
|

hello!exit@plt

|

0000000000400558

|
|

*hello!printf@plt

| |
|

|

*hello!sleep@plt

|

| |
|

|

*hello!getchar@plt

|

| |
|

libc-2.27.so!exit

|

000007ff9c8898127

|

表5-2 执行流程

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

5.7 Hello的动态链接分析

如果程序调用了一个由共享库定义的函数,编译器没有办法预测这个函数的运行时地址,因为定义它的共享库函数可以在运行时被加载到任意位置。GNU编译系统使用了一种叫做延迟绑定的技术,将过程地址的绑定推迟到第一次调用这个函数时。

具体是通过GOT和PLT来实现,GOT是数据段的一部分,PLT是代码段的一部分。

GOT是一个数组,每个条目是8字节的地址,GOT[0]GOT[1]保存的是动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在ld-linux.so模块中的入口点。

PLT是一个数组,每个条目是16字节的代码,从PLT[2]开始的条目调用用户代码调用的函数。

在hello文件中,plt节的起始位置是0x4004a0,除去0、1,从PLT[2]开始的条目为用户代码调用的库函数,这里以exit()函数为例,使用edb查看plt第3个条目的相关代码,第一条指令一定为跳转到GOT中的相关条目,由于在dl_init之前,所以GOT中条目指向的应该是PLT条目的下一条地址。

[图片上传失败...(image-ef444d-1546768649790)]

图5-20 plt起始地址

从PLT[2]开始查看,其对应的GOT条目地址为601020,edb查看对应条目内容

[图片上传失败...(image-86467a-1546768649790)]

图5-21 printf对应的PLT条目

[图片上传失败...(image-2773d0-1546768649790)]

图5-22 GOT中的地址,为PLT下一条指令

在调用dl_init函数后再次查看相关的GOT条目

[图片上传失败...(image-877b9b-1546768649790)]

图5-23 init后GOT

可以看见,经过init函数调用后GOT中GOT[1],GOT[2]发生了变化,其中GOT[2]中保存的是动态链接器的入口地址,edb中查看相关动态链接器,经过init函数初始化后,动态链接器被加载入GOT中,但是此时函数的地址仍然没有加载,延迟绑定技术使得直到程序调用函数时,才会动态重定位函数的位置。

[图片上传失败...(image-19f73c-1546768649790)]

图5-24 动态连接器

为了说明延迟绑定技术,图5-25是init后,没有执行到第一个函数之前的GOT[2]内容,此时内容仍然为PLT条目的下一条地址。

[图片上传失败...(image-e0a60d-1546768649790)]

图5-25 未执行printf前

执行到printf后,GOT[2]内容发生了变化,此时就是printf在虚拟内存中的真正地址了,而且之后调用printf就不用像上述再执行一遍了。

[图片上传失败...(image-ec2aca-1546768649790)]

图5-26 执行printf后

5.8 本章小结

本章讨论了链接过程是如何实现的,介绍了可重定位文件和可执行文件的ELF格式,经过链接处理,hello变成了可执行文件,而此时还有动态链接内容没有完成,直到程序在运行到函数时,动态链接器才将相关函数的地址确定。此时,hello程序真正的完整了。

第6章 hello进程管理

6.1 进程的概念与作用

6.1.1. 进程的概念

进程是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文中,上下文是由程序正确运行所需要的状态组成的。这个状态包括存放在内存中的程序的代码和数据,栈,通用目的寄存器内容,PC,环境变量等。

6.1.2. 进程的作用

提供一个假象,当前程序为系统中唯一运行的程序,程序仿佛独占处理器和内存,程序中的代码和数据好像是系统内存中唯一的对象,这给操作系统和用户管理程序带来了极大的便利。

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

6.2.1.shell-bash 的作用

shell是一种交互型的应用级程序,它为用户提供了一种界面,用户通过这个界面运行应用程序。

6.2.2. shell-bash 的处理流程

shell的处理流程为一系列的读/求值步骤,然后终止,读步骤读取来自用户的命令行,求值步骤对命令行进行解析,并代表用户运行程序。

6.3 Hello的fork进程创建过程

作为hello父进程的shell程序,在接受到用户输入的命令行

./hello 1163450201 jjd

后,shell程序会先对命令行进行解析,首先判断这不是一个shell的内置命令,然后认为./是要运行当前目录下的一个名为hello的程序且参数为:1163450201 jjd

接着shell会调用fork函数创建一个子进程,新建的子进程继承了父进程所打开的文件(屏幕、键盘等),创建父进程数据的一个副本(独立的),与父进程有着相同的虚拟空间地址(独立的),同时将子进程的PID设置为与父进程不同。至此,hello的fork进程创建完毕。

6.4 Hello的execve过程

创建进程完成后,子进程中会有一个关于pid的判断逻辑,这是因为fork程序在创建子进程后,对于父进程会返回子进程的pid,而子进程则会返回0.通过判断语句

if ( pid == 0)

如果判断出位于子进程中,程序就会执行execve函数将hello程序加载到当前进程的上下文中。并且同时带有参数列表argv和环境变量列表envp,同时execve函数调用一次,从不返回。加载了hello之后,它调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数,执行main。

6.5 Hello的进程执行

hello进程的执行通过上下文切换来实现,控制权通过时间片分配来实现。

操作系统内核通过使用一种称为上下文切换的异常控制流来实现多任务。多任务的实现基础是并发。多个逻辑流并发的执行成为并发,一个进程和其他进程轮流运行的概念成为多任务。一个进程执行它的控制流的一部分的每一个时间段称为时间片。多任务也成为时间分片。

说回上下文,内核为每个进程维持一个上下文。上下文就是内核重新启动一个被抢占的进程所需的状态。在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种叫做调度。

内核通过上下文切换机制实现将控制转移到新的进程。上下文切换

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

2. 恢复某个之前被抢占的进程的上下文

3. 控制转移给新的进程

hello进程执行就是在上述基础上实现的。上下文切换时,操作系统会进入内核模式,而结束时进入用户模式。这里内核模式指的是进程可以执行指令集中任何操作,访问任何数据的模式。内核模式只有通过异常和系统调用进入,其余都是用户模式。当然hello进程的执行也是在用户模式中。

[图片上传失败...(image-41af27-1546768649790)]

图6-1 进程上下文切换

6.6 hello的异常与信号处理

hello执行中可能遇到的异常以及对应的信号以及处理办法

一般来说,操作系统中的异常为下面四种

|

类别

|

原因

|

异步/同步

|

返回此行为

|
|

中断

|

来自I/O设备的信号异常

|

异步

|

总是返回到下一条地址

|
|

陷阱

|

有意的异常

|

同步

|

总是返回到下一条指令

|
|

故障

|

潜在可以恢复的错误

|

同步

|

可能返回到当前地址

|
|

终止

|

不可恢复的错误

|

同步

|

不返回

|

hello中都有可能遇到,但是这里只能进行其中的几种测试,例如回车,Ctrl-Z,Ctrl-C等

a. 回车

[图片上传失败...(image-4dea54-1546768649790)]

图6-2 回车

回车并不会对程序运行造成什么影响,回车仅仅影响了shell的输出,导致多了几个空行。。。最后这些回车会进入缓冲区,作为shell的命令行被读取。

b. Ctrl-C

[图片上传失败...(image-846015-1546768649790)]

图6-3 终止信号

Ctrl-C直接导致了程序的终止,这是因为Ctrl-C会发送一个键盘的中断信号SIGINT给hello,而hello接受后会执行信号处理程序使得程序退出。

c. Ctrl-Z

Ctrl-Z发送一个来自终端的停止信号SIGTSTP使得程序停止,这是可以使用linuxshell自带的一些指令对其进行观察。这种异常是陷阱。

[图片上传失败...(image-d7e0e1-1546768649790)]

图6-4 输入CTRLZ后显示hello已停止

[图片上传失败...(image-ff2271-1546768649790)]

图6-5 输入命令ps查看进程

[图片上传失败...(image-39b544-1546768649790)]

图6-6 使用jobs命令查看当前作业,注意此时hello是停止的

[图片上传失败...(image-26f433-1546768649790)]

[图片上传失败...(image-30346b-1546768649790)]

图6-7 pstree命令,查看进程树

[图片上传失败...(image-dcf99-1546768649790)]

图6-8 输入命令fg,恢复前台进程hello,注意到此时hello恢复运行

[图片上传失败...(image-5ef687-1546768649790)]

图6-9 输入命令kill 向hello发送终止信号

6.7本章小结

本章说明了hello进程的创建过程和和加载过程,也介绍了hello的信号和异常处理。hello此时已经存在于进程中,经过这些步骤,hello运行了起来。

第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:

CPU所生成的地址。逻辑地址是内部和编程使用的、并不唯一。例如,你在进行C语言指针编程中,可以读取指针变量本身值(&操作),实际上这个值就是逻辑地址,它是相对于你当前进程数据段的地址(偏移地址),不和绝对物理地址相干。

物理地址:

加载到内存地址寄存器中的地址,内存单元的真正地址。在前端总线上传输的内存地址都是物理内存地址,编号从0开始一直到可用物理内存的最高端。这些数字被北桥(Nortbridge chip)映射到实际的内存条上。物理地址是明确的、最终用在总线上的编号,不必转换,不必分页,也没有特权级检查

线性地址:

线性地址空间是指一个非负整数地址的有序集合,例如{0,1,2,3……}

虚拟地址:

以线性地址为基础,CPU从地址空间中生成的地址成为虚拟地址,hello反汇编文件中,指令前面的地址就是虚拟地址

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

Linux将虚拟内存组织为一些区域(也称为段)的集合(下面统称为段)。一个段就是已经存在着的虚拟内存的连续片。这些页是以某种方式相关联的。例如hello中的代码段,数据段、堆,共享库段等。每个存在的虚拟页面都保存在某个段中。而不属于某个段的虚拟页面是不存在的,并且不能被进程引用。段允许你虚拟地址空间有间隙。内核为系统中每个进程维护这一个单独的任务结构(task_struct)。任务结构中的元素包含或指向内核运行该进程所需要的信息。任务结构中的一个条目指向mm_struct,描述了虚拟内存的当前状态。pgd指向第一级页表的基址,mmap指向一个vm_area_structs的链表,每个vm_area_structs都描述了当前虚拟地址空间的一个段。pgd存放在CR3控制寄存器中。

其中

  1. vm_start:指向段的开始处

  2. vm_end:指向段的结束处

  3. vm_prot:描述段的页的读写权限

  4. vm_flags:描述段的页面是共享的还是私有的

  5. vm_next:指向下一个段结构

[图片上传失败...(image-2f3fc9-1546768649789)]

图7-1 虚拟内存的段管理

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

虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元数组。VM系统通过将虚拟内存分割为称为虚拟页(VP)的大小固定的块来处理这个问题,物理内存被分割为物理页(PP),大小同VP。虚拟页面分为三种状态

1. 未分配的:未分配块没有任何数据和它们关联。

2. 缓存的:当前已缓存在物理内存中的已分配页

3. 未缓存的:未缓存在物理内存中的已分配页

虚拟页的大小通常在4KB-2MB,为了管理这也虚拟页,有了页表的概念。页表就是一个页表条目的数组。每个页在页表的一个固定偏移量处都有一个PTE,PTE由一个有效位和n为地址字段组成。地址字段表示物理页的起始地址。页表存放在内存中,通过MMU,可以实现从虚拟地址翻译到物理地址的过程。虚拟地址可以分为VPN和VPO,其中VPO的位数取决与页的大小。通过VPN可以确定虚拟页号,找到相应的物理页号PPN之后,与VPO合并即可得到对应的物理地址。

[图片上传失败...(image-4bc7b8-1546768649789)]

图7-2 虚拟页和物理页映射

[图片上传失败...(image-e98715-1546768649789)]

图7-3 通过VA读取PT的PPN

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

因为从内存中访问页表速度还是很慢,所以之后人们又在MMU中增加了页表的缓存TLB。TLB通常有着很高的相连度。TLB地址分为TLBI,TLB索引和TLBT,TLB标记。

[图片上传失败...(image-cd13a0-1546768649789)]

图7-4 VPN的构成

[图片上传失败...(image-6b6da8-1546768649789)]

图7-5 带有TLB的VA翻译过程

如果出现页表数量非常多的情况,不仅会大大占用内存,还会对寻找虚拟页带来巨大的开销,所以提出了多级页表的概念,每一级的页表内容是下一级页表的基址,当页表内容为空时,就不创建下一级页表,等到需要时在进行创建,节约内存空间。Intel i7处理器支持4级页表,在4级页表中,从VA到PA的处理过程如下图

[图片上传失败...(image-9c1c85-1546768649789)]

图7-6 i7四级页表的VA到PA

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

缓存的出现是为了缓解存储设备和CPU之间巨大的速度差异。处理器对内存数据的访问,一般是通过cache进行。具体过程为

通过地址解析出缓存的索引和偏移,对缓存进行访问,匹配标记查找是否含有相关的字,如果命中,则将数据发送给CPU,如果没有命中,则访问下一级缓存,取出这个字,存入高一级缓存,返回数据给CPU

地址一般分为CO、CI、CT

[图片上传失败...(image-c2197a-1546768649789)]

图7-7 PA的构成和寻找数据示意图

缓存一共有三种形式:全相联高速缓存、组相联高速缓存、直接映射高速缓存。其中最本质的区别就是同一个组中有多少行。

7.6 hello进程fork时的内存映射

当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进进程的mm_struct、段结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个段结构都标记为私有的写时复制。

私有写式复制保证了对数据的操作不会相互干扰。

[图片上传失败...(image-11c313-1546768649789)]

图7-8 私有的写时复制

7.7 hello进程execve时的内存映射

以hello程序举例,execve函数在当前进程中(子进程)加载并运行包含在可执行文件hello.out中的程序,加载并运行hello经过了以下几个步骤

1. 删除已存在的用户区域

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

2. 映射私有区域

为新程序的代码数据、bss和栈段创建新的段结构

3. 映射共享区域

动态链接共享库内容

4. 设置程序计数器

设置上下文的PC,使其指向当前的入口。

[图片上传失败...(image-fec8ea-1546768649789)]

图7-9 加载器的用户地址空间映射

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

如7.2节所述,Linux维护着进程的虚拟内存,假设MMU在试图翻译某个虚拟地址A时,触发了一个缺页。这个异常导致控制转移到内核的缺页处理程序。程序会执行以下几个步骤

1. A地址是否合法,如果地址不合法,访问到了段之外的地址,那么就会触发一个段错误,从而终止进程。

2. 试图进行的内存访问是否合法,即进程是否有读、写或执行这个段内页面的权限,如果不合法,就会触发一个保护异常,终止程序。

3. 如果上述情况都没有发生,那就是一个正常的缺页,选择一个牺牲页面,如果页面没有修改过,就正常覆盖,如果修改过,换出在覆盖。当缺页处理程序返回时,CPU重新启动引起缺页的指令,再次查找。

[图片上传失败...(image-5e1317-1546768649789)]

图7-10 缺页处理方式

7.9动态存储分配管理

原理:

动态内存分配器维护着一个进程的虚拟内存区域,称为“堆”。对于每个进程,内核维护着一个变量brk,指向堆顶。

分配器将堆视为一组不同大小块的集合来维护。每个块是一个连续的虚拟内存片(chunk),这个块要么是已分配的,要么是空闲的。已分配的块显式的保留为应用程序使用,空闲块可用来分配。空闲块保持空闲,直到它被应用程序显式的分配。已分配块保持分配,直到被应用程序显式的释放或者被分配器隐式的释放。

由于Linux中使用的是分离适配方法,因此在这里介绍显示空闲链表。

[图片上传失败...(image-44f771-1546768649789)]

 [图片上传失败...(image-96088f-1546768649789)] 

图7-11 分配块 图7-12 空闲块

在这里,堆被组织成一个双向空闲链表,在每个空闲块中,包含着一个pred前驱指针和succ后继指针。这使得首次适配时间从块总数的线性时间减少到了空闲块的线性时间。空闲链表的排序策略有两种

  1. 后进先出LIFO的顺序维护链表

将新释放的块放置在链表的开始处,使用首次适配和LIFO排序策略,分配器会检查先检查最近使用的块。

  1. 地址顺序维护链表

链表中的每一个块的地址都小于它的后继地址,释放一个块需要线性时间来搜索定位适合的前驱。

地址排序的首次排序要比LIFO有更高的内存利用率。

当一个应用请求k字节的块,分配器搜索空闲链表,找到适合大小的块,这里有三种搜索放置策略

  1. 首次适配

  2. 下一次适配

  3. 最佳适配

一旦分配器找到合适的空闲块,它通常将空闲块分割为两部分,一个是已分

配块,另一个是空闲块。或者,如果空闲块与请求匹配较好,会将整个空闲块分配给它,尽管会产生内部碎片。

如果空闲链表中没有足够的空间,分配器会申请新的堆内存或者合并空闲块。分配器通过检查当前块的前一个块的脚部,获取前一个块的位置和状态,决定是否合并。一般有四种情况

  1. 前后都分配

  2. 前分配,后空闲

  3. 前空闲,后分配

  4. 前后空闲

不同情况对应不同处理方式,都是把空闲中的多出来的头部脚部去掉。

7.10本章小结

本章介绍了在存储层面hello是如何被存储访问的和Linux是如何管理虚拟内存的。

Linux将磁盘文件抽象为虚拟内存用来管理,为了管理这些虚拟页,又建立了页表PTE,为了更加高效的读取PTE,又建立了TLB。通过MMU,将VA转换为PA,实现对数据的访问。最后为了实现主动对内存的管理,简单介绍了动态内存管理的办法。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:

文件是C语言和Linux管理的思想,所有的IO设备都被抽象为文件,所有的输入输出都作为对文件的操作。这个设备映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得输入和输出都能以一种统一且一致的方式的来执行。

8.2 简述Unix IO接口及其函数

Linux以文件的方式对I/O设备进行读写,将设备均视为文件。对文件的操作,内核提供了Unix I/O接口。

打开文件:int open(char *filename, int flags, mode_t mode);

关闭文件:int close(int fd);

读文件:ssize_t read(int fd, void *buf, size_t n);

写文件:ssize_t write(int fd, const void *buf, size_t n);

8.3 printf的实现分析

printf实现代码如下:

  1. static int printf(const char *fmt, ...)

  2. {

  3. **va_list** args;  
    
  4. **int** i;  
    
  5. va_start(args, fmt);  
    
  6. write(1,printbuf,i=vsprintf(printbuf, fmt, args));  
    
  7. va_end(args);  
    
  8. **return** i;  
    
  9. }

其中*fmt是格式字符串,后面是变量,也就是打印用的变量。va_list型变量args表示参数,va­_start函数是取到fmt中的第一个参数的地址,write为系统级函数,vsprintf函数用来格式化,该函数返回值打印出字符串的长度,同时将printbuf根据格式串进行格式化,产生格式化输出。write函数调用syscall实现系统调用陷入内核,使内核执行打印操作。

从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.。字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

getchar函数的实现如下

  1. int getchar(void)

  2. {

  3. **char** c;  
    
  4. **return** (read(0,&c,1)==1)?(unsigned **char**)c:EOF  
    
  5. }

getchar函数通过使用read这个系统级函数返回字符。read第一个参数表示标准输入(0),最后参数表示读入字符的数量。

read函数也是使用了syscall陷入系统内核,键盘中断处理程序会等待键盘输入,read从缓冲区读取。

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

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

8.5本章小结

Linux将IO设备抽象为文件,提供了IO接口。通过接口,程序能够简单的对设备进行调用,这就是Linux系统的思想之一。printf函数通过系统级函数write函数对标准输出进行输出,getchar使用read来对标准输入进行接收。

结论

hello从诞生到毁灭经过了一系列过程,它的一生的过程有:

1. 通过IDE输入代码形成了c格式文件hello.c

2. 通过预处理器,编译器,汇编器,链接器最终生成可执行文件hello

3. 通过shell的fork函数为hello创造了能够运行的上下文,通过execve函数将hello加载入进程之中,使得hello真正的运行了起来

4. 通过Linux的虚拟内存管理,操作系统的时间片分配,信号接受处理机制,IO输入输出等,hello程序在计算机中游刃有余的输出到屏幕,读取共享库中的代码并使用。

5. 通过Linux的进程回收机制,hello运行完后,结束了它短暂的一生。

一个简单的hello程序如果要成功运行,需要经过很多很多的步骤,操作系统与硬件相互配合,精密耦合,才能保证程序成功的运行。

操作系统是一个高度复杂的精密系统,每一步都不可或缺,用户模式和内核模式保证了工作处理的互不干扰,系统调用保证了系统安全的同时也允许用户能够使用简答的系统功能。高度抽象的思想贯彻到了系统的每一处。

硬件系统上,磁盘与CPU之间巨大的速度差距使得缓存的诞生,多级缓存保证了速度的逐渐稳定过度。同样也是缓存的思想,TLB提高了页表的访问速度,缓冲区使得输入可以不用与读取的速度匹配,寄存器作为最高级别L0的缓存,对于CPU的计算,汇编的运行做出了巨大贡献。

包装的思想无处不在,从最基本二进制抽象为十六进制,到代码包装为函数,到函数被包装为简单的接口,在层层包装为模块,组成了系统,包装也可以说为抽象,随意啦。

系统协调一切,系统领导一切,每个稳定运行的程序背后,都是操作系统在背后的默默付出。系统协调软硬件无缝合作,使得二进制10在总线和芯片上翻涌着浪花。

谁能想到,简单的hello却蕴含着这么多呢。

附件

  1. hello.c

hello程序的源程序,用来进行接下来的处理

  1. hello.i

hello.c文件经过预处理器处理后的文件,替换了c文件中的#开头语句, 该文件用于接下来的编译

  1. hello.s

hello.i文件经过编译后生成的文件,将源代码换为汇编代码,该文件用于接下来汇编器的处理

  1. hello.o

hello.s文件经过汇编器处理生成的文件,生成可重定位文件,为经过链接处理,该文件用于接下来和其他动态库或静态库链接生成可执行文件

  1. hello

经过gcc命令通过hello.c生成

  1. hello

经过ld命令链接器最终生成的文件,可以用来运行和研究。

参考文献

[1] 兰德尔E, 布莱恩特. 深入理解计算机系统[M]. 北京:机械工业出版社, 2016..

[2] rabbit_in_android.虚拟地址、逻辑地址、线性地址、物理地址[EB/OL]. https://blog.csdn.net/rabbit_in_android/article/details/49976101.2015-2-22-2018-12-31.

[3] wang_xya.[EB/OL]. https://blog.csdn.net/wang_xya/article/details/43985241.2015-2-28-2018-12-31.

[4] Pianistx.[EB/OL]. https://www.cnblogs.com/pianist/p/3315801.html.2013-9-11-2018-12-31.

上一篇下一篇

猜你喜欢

热点阅读