计算机系统大作业:程序人生-Hello‘s P2P

计算机系统

大作业

题     目  程序人生-Hello’s P2P 

专       业  计算机系                

学     号  120L021303              

班     级  2003002                 

学       生  苏泓斌                

指 导 教 师  史先俊                   

计算机科学与技术学院

2021年5月

摘  要

本文介绍了一个hello.c源程序在Linux系统中的一个完整的生命周期,从编译到运行到终止的全过程,并以此为依托,概述了计算机系统的一些重要机制,包含了计算机科学的一些经典思想。本文详细介绍了从.c源文件生成可执行文件的预处理、编译、汇编、链接四个阶段,同时介绍了计算机系统的两个及其重要的抽象机制:进程和虚拟地址空间,并简要的概述了I/O管理。

关键词:计算机系统;Linux;hello程序                           

目  录

第1章 概述... - 5 -

1.1 Hello简介... - 5 -

1.1.1 P2P:From Program to Process. - 5 -

1.1.2 020:From Zero-0 to Zero-0. - 5 -

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

1.2.1 硬件环境... - 5 -

1.2.2 软件环境... - 6 -

1.2.3 开发与调试工具... - 6 -

1.3 中间结果... - 6 -

1.4 本章小结... - 6 -

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

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

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

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

2.4 本章小结... - 8 -

第3章 编译... - 9 -

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

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

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

3.3.1 数据... - 9 -

3.3.2 赋值... - 10 -

3.3.3算术操作... - 10 -

3.3.4 关系操作... - 11 -

3.3.5 数组操作... - 11 -

3.3.6 控制转移... - 12 -

3.3.7 函数操作... - 12 -

3.4 本章小结... - 14 -

第4章 汇编... - 15 -

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

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

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

4.3.1 ELF头... - 15 -

4.3.2 节头部表... - 16 -

4.3.3 重定位节... - 17 -

4.3.4 符号表... - 18 -

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

4.5 本章小结... - 20 -

第5章 链接... - 21 -

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

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

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

5.3.1 ELF头... - 21 -

5.3.2 节头部表... - 22 -

5.3.3 符号表... - 24 -

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

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

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

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

5.8 本章小结... - 29 -

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

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

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

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

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

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

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

6.6.1 异常... - 33 -

6.6.2 信号以及处理... - 33 -

6.6.3 实际测试... - 34 -

6.7本章小结... - 36 -

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

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

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

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

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

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

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

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

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

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

7.10本章小结... - 42 -

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

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

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

8.2.1 Unix IO接口... - 43 -

8.2.2 Unix IO函数... - 44 -

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

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

8.5本章小结... - 46 -

结论... - 46 -

附件... - 48 -

参考文献... - 49 -

第1章 概述

1.1 Hello简介

根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。

1.1.1 P2P:From Program to Process

·Program:在linux系统中,我们先打开vim等文本编辑器,在我们敲击键盘的过程中,字符被读入寄存器,再被存入内存中。当我们保存hello.c文件并退出,程序文本被交换到磁盘。

·Process:hello.c经过cpp的预处理、cc1的编译、as的汇编、ld的链接,最终成为可执行目标文件。然后在shell中键入运行命令,进程管理为其fork进程,hello.c就从程序变为了进程。

1.1.2 020:From Zero-0 to Zero-0

·为hello创建进程后,利用execve把hello的内容加载到子进程的地址空间中,映射虚拟内存,进入程序入口后开始载入物理内存。在这个过程中,涉及到虚拟内存:在fork时,操作系统仅仅给子进程复制各种数据结构,如页表等。进入 main 函数执行目标代码, CPU 为运行的 hello 分配时间片执行逻辑控制流。

·当子进程执行return语句后,它保持一种已经终止的状态,向shell发送SIGCHLD信号,等待shell对其进行回收。当shell调用waitpid指示操作系统将其回收,内核删除相关数据结构后,hello的生命周期便结束了。

1.2 环境与工具

1.2.1 硬件环境

X64 CPU(AMD Ryzen 7 4700U);2 GHz;16G RAM;256GHD Disk 以上

1.2.2 软件环境

Windows10 64位;Vmware 16pro;Ubuntu 20.04 LTS 64位

1.2.3 开发与调试工具

Visual Studio 2010 64位以上;CodeBlocks 64位;vi/vim/gedit+gcc;gcc;gdb;edb

1.3 中间结果

文件

作用

hello.i

hello.c预处理之后的文本文件

hello.s

hello.i编译后的汇编文件,文本文件

hello.o

hello.s汇编之后的可重定位目标文件,二进制文件

hello

链接之后的可执行目标文件,二进制文件

hello.out

hello反汇编之后的可重定位文件

表1-1 中间结果文件

1.4 本章小结

本章简要介绍了hello的P2P和020的整个过程,写出了本文所用到的软硬件环境和开发与调试工具,列出了整个过程所产生的中间结果。本章是全文的概述,接下来的章节则是对各部分详细的论述。

第2章 预处理

2.1 预处理的概念与作用

预处理(preprocessing)指的是预处理器(cpp)根据以字符#开头的命令(如#include、#define、#pragma等),修改原始的C程序,最后生成.i文本文件的过程。C预处理器扩展源代码,插入所有用#include命令指定的文件,并扩展所有用#define声明指定的宏[1]。

预处理能够帮助程序员节省工作量、使程序更易读、便于维护。

1. 处理头文件包含指令头文件包含指令如#include "FileName"或者#include 等。该指令将头文件中的内容直接插入到hello.i中,以供编译程序对之进行处理。

2. 处理条件编译指令如#ifdef,#ifndef,#else,#elif,#endif等。预编译程序将根据有关的文件,将那些不必要的代码过滤掉。

3. 处理宏定义指令。用实际值替换用#define 定义的字符串。

4. 预编译程序可以识别一些特殊的符号如LINE、FILE等。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。

2.2在Ubuntu下预处理的命令

命令:cpp hello.c hello.i

由图2-1可以看出,预处理过后,文件夹中产生了一个名为hello.i的新文件。

图 2-1 预处理命令

2.3 Hello的预处理结果解析

可以发现整个程序已经拓展为3060行,原来hello.c的程序出现在3046行及之后。在这之前出现的是头文件 stdio.h unistd.h stdlib.h 的依次展开,其中包括大量的typedef,如typedef unsigned char __u_char;还包括600多行的枚举(enum)类型,以及标准C的函数原型,如extern int printf (const char *__restrict __format, …);标准输入输出和错误也在这里被定义(extern FILE *stdin;extern FILE *stdout;extern FILE *stderr;)。

cpp 对头文件中define宏定义递归展开。所以最终的.i文件中是没有#define的;发现其中使用了大量的#ifdef #ifndef条件编译的语句,cpp会对条件值进行判断来决定是否执行包含其中的逻辑。预编译程序可识别一些特殊的符号,预编译程序对在源程序中出现的这些串将用合适的值进行替换。

图2-2 hello.i文件部分内容截图

2.4 本章小结

本章主要介绍了预处理的概念及其作用(包括头文件的展开、宏替换、去掉注释、条件编译),并在linux系统下以hello.c为例子,使用命令生成了hello.i文本文件,通过对hello.i内容的解析,进一步了解了预处理的内涵和流程。

第3章 编译

3.1 编译的概念与作用

编译的概念:编译器将文本文件 hello.i 翻译成文本文件 hello.s,它包含一个汇编语言程序。其以高级程序设计语言书写的源程序作为输入,而以汇编语言或机器语言表示的目标程序作为输出。这个过程称为编译,同时也是编译的作用。

汇编语言为不同种类的语言提供了相同的形式,其指令与处理器的指令集类似,更贴近底层,便于汇编器将其转换为机器码供机器执行。编译程序的基本功能是把源程序(高级语言)翻译成目标程序。除了基本功能之外,编译程序还具备语法检查、调试措施、修改手段、覆盖处理、目标程序优化、不同语言合用以及人际联系等重要功能。

3.2 在Ubuntu下编译的命令

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

由图3-1可以看出,编译过后,文件夹中产生了一个名为hello.s的新文件。

图3-1 编译命令

3.3 Hello的编译结果解析

3.3.1 数据

1、常量

在if (argc != 4)条件语句中,常量4保存在.text段中,作为指令的一部分,如图3-2所示。同理可得,.L4的常量0 8 1 2 3亦如此。

图3-2 常量4的调用

而在下述的printf函数中

printf("用法: Hello 学号 姓名 秒数!\n");

printf(), scanf()中的字符串则被存储在.rodata节中。

图3-3 只读数据区的字符串常量

2、变量

main函数声明了一个局部变量i,i被初始化为0后,该局部变量存储在地址为栈上%rbp-4的位置。图3-4为对应的汇编代码。

图3-4 局部变量初始化的汇编代码

3.3.2 赋值

程序中的赋值操作:i = 0,此赋值操作在汇编代码中使用mov指令来实现,又根据数据的类型可以分为movb movw movl movq。由图3-4可以看出此赋值操作将一个四个字节大小的数据赋值给变量i。

3.3.3算术操作

在循环体中,每次循环都i++,使用++自加算术操作符

for(i=0;i<8;i++)

体现在汇编代码中,则是每次循环结束都使用addl指令,使存储在栈上的变量i的值加1。

图3-5 循环体中的算术操作

3.3.4 关系操作

1、argc != 4;是条件语句中的条件判断:argc != 4,进行编译时,这条指令被编译为:cmpl $4, -20(%rbp),同时这条cmpl的指令还有设置条件码的作用,当根据条件码来判断是否需要跳转到分支中。

图3-6 !=关系操作

2、i < 8,作为判断循环条件,在汇编代码被编译为:cmpl $7, -4(%rbp),计算 i-7然后设置条件码,为下一步 jle 利用条件码进行跳转做准备。

图3-7 <关系操作

3.3.5 数组操作

hello.c中唯一的数组char *argv[]是作为main函数的第二个参数,数组的每个元素都是一个指向字符类型的指针。数组的起始地址存放在栈中-32(%rbp)的位置,因为char* 数据类型占8个字节,前两次调用 argv[1] (%rbp - 8)和 argv[2] (%rbp - 16)传给printf,第三次调用传最后一个参数给sleep函数。图3-8用三种不同的颜色分别标出了对数组的三次引用。

图3-8 数组操作

3.3.6 控制转移

1、if/else

判断argc是否为4,如果不等于则执行if语句,否则跳转到.L4循环体执行代码。

    

图3-9 if条件控制转移C语言代码                        图3-10 if条件控制转汇编代码

2、for

for(i=0;i<8;i++),通过每次判断i是否满足小于8来判断是否需要跳转至循环语句.L4中。

      

图3-11 for控制转移C语言代码                   图3-12 for控制转移汇编代码

3.3.7 函数操作

X86-64中,过程调用传递参数规则:第1~6个参数一次储存在%rdi、%rsi、%rdx、%rcx、%r8、%r9这六个寄存器中,剩下的参数保存在栈当中。

1、main函数

分析图3-13的汇编代码:

参数传递:传入参数argc和argv[],分别用寄存器%rdi和%rsi存储。

函数调用:被系统启动函数调用。

局部变量:循环变量i。

函数返回:设置%eax为0并且返回,对应return 0 。

 

图3-13 main函数和.L3的汇编代码

2、printf函数

hello.c共有两处调用了printf()函数。

(1) 第一次是在if条件控制语句中,如果参数个数不是4,则从只读数据区中取字符串输出。

参数传递:call puts只传入了只读数据区的字符串参数首地址。

函数调用:if判断满足条件后调用。

函数返回:无。

图3-14 第一次调用printf函数的汇编代码

(2) 第二次是在for循环体中,每次循环调用一次printf函数。

参数传递:for循环中call printf时传入了 argv[1]和argc[2]的地址。

函数调用:for循环中被调用。

函数返回:无。

图3-15 第二次调用printf函数的汇编代码

3、exit函数:

参数传递:传入的参数为1,再执行退出命令。

函数调用:if判断条件满足后被调用。

函数返回:无。

图3-16 exit函数调用汇编代码

4、sleep函数:

参数传递:传入参数atoi(argv[3])。

函数调用:for循环下被调用,call sleep。

函数返回:无。

图3-17 sleep函数调用汇编代码

5、getchar函数:

函数调用:在main函数最后被调用,call getchar。

3.4 本章小结

本章主要介绍了编译的概念以及过程和作用。同时通过示例函数表现了C语言中各种类型和操作所对应的的汇编代码。对我们的源程序hello.c编译后得到的汇编语言程序中的各种数据、操作做了详细的解析。介绍了汇编代码如何实现变量、常量、传递参数以及分支和循环。

第4章 汇编

4.1 汇编的概念与作用

汇编器as接受.s作为输入,以可重定位目标文件.o作为输出。即驱动程序运行汇编器as,将汇编语言(这里是hello.s)翻译成机器语言(hello.o)的过程称为汇编。

可重定位目标文件包含二进制代码和数据,其形式可以在编译时与其他可重定位目标文件合并起来,创建一个可执行目标文件,从而被加载到内存并执行[1]。所以汇编的作用就是将汇编语言转换成机器可以理解的机器语言指令。

4.2 在Ubuntu下汇编的命令

命令:as hello.s -o hello.o

由图4-1可以看到,经过as汇编后,文件夹中出现了名为hello.o的新文件。

 

图4-1 as汇编命令

4.3 可重定位目标elf格式

命令:readelf -a hello.o

-a显示全部信息,也可以换成其他选项,如-h只显示ELF头的信息,-S只显示节头部表的信息。

4.3.1 ELF

ELF头描述生成该文件的系统的字的大小和字节顺序、帮助链接器语法分析和解释目标文件的信息。Magic 描述了生成该文件的系统的字的大小和字节顺序,ELF 头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括 ELF头的大小、目标文件的类型、机器类型、字节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量等信息。比如ELF64(ELF 64位的可执行程序);2’s complement,little endian即补码表示,小端法;REL(Relocatable file即可重定位目标文件);运行机器为AMD x86-64;节头开始为文件开始处的1240字节偏移处。

 

图4-2 可重定位目标文件的ELF头

4.3.2 节头部表

节头部表用于描述不同节的位置和大小,图4-3的属性分别是节名、类型、地址(此时暂时未被分配均为0)、偏移量(节相对于文件开始的偏移)、节大小、表项(Entry)大小、flags(节属性)、(与其他节的)关联、附加节信息、对齐(2的Align次方)。由于是可重定位目标文件,所以每个节都从0开始,用于重定位。

 

图4-3 可重定位目标文件的节头部表

4.3.3 重定位节

一个.text节中位置的列表,当连接器把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。另一方面,调用本地函数的指令则不需要修改。

本程序需要重定位的信息有:.rodata中的模式串,puts,exit,printf,slepsecs,sleep,getchar这些符号。

 

图4-4 可重定位目标文件的重定位节

4.3.4 符号表

.symtab是一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。name是符号名称,对于可冲定位目标模块,value是符号相对于目标节的起始位置偏移,对于可执行目标文件,该值是一个绝对运行的地址。size是目标的大小,type要么是数据要么是函数。Bind字段表明符号是本地的还是全局的。

图4-5 可重定位目标文件的符号表

4.4 Hello.o的结果解析

命令:objdump -d -r hello.o

 

图4-6 objdump反汇编结果

分析hello.o的反汇编,并与第3章的 hello.s进行对照分析:

操作数的表示:hello.o反汇编代码中的操作数是十六进制的,hello.s中的操作数是十进制的。

分支转移:跳转语句之后,hello.s中是.L2和.LC1等段名称,而反汇编代码中跳转指令之后是相对偏移的地址,即间接地址,更适合被加载到内存中工作。

函数调用:hello.s中,call指令使用的是函数名称,而反汇编代码中call指令使用的是main函数的相对偏移地址。要留到链接阶段进行重定位符号引用,才会填上相对偏移量。此外,由于在编译阶段没有保留符号的名字,函数调用都被写为了<main+offset>的形式。

4.5 本章小结

本章简述了汇编的概念,介绍了汇编的命令行,分析了ELF可重定位目标文件的格式,解析了对汇编后的文件hello.o进行反汇编之后的内容,并与hello.s文件进行了对比,分析了从汇编语言到机器语言的一一映射关系。

5章 链接

5.1 链接的概念与作用

链接器将可重定位目标代码文件和一些库函数的代码合并,产生最终的可执行目标文件hello,其可以直接被复制到内存执行。链接可以执行于编译时,也可以执行于加载时,也可以在运行时由应用程序来执行。

链接将多个重定位目标文件(或静态/动态库)整合到一起,并且修改符号引用,输出一个可执行目标文件。链接使得一个较大的程序可以被分解成许多模块来编写,并最终合并为一个可执行程序。

5.2 在Ubuntu下链接的命令

命令:

ld -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 /usr/lib/gcc/x86_64-linux-gnu/9/crtbegin.o hello.o -lc /usr/lib/gcc/x86_64-linux-gnu/9/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -z relro

由图5-1可以看出,链接命令之后,文件夹出现了一个名为hello的新文件。

图5-1 链接命令

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

命令:readelf -a hello

-a显示全部信息,也可以换成其他选项,如-h只显示ELF头的信息,-S只显示节头部表的信息。

5.3.1 ELF

将图5-2与链接前的ELF头作比较,发现Type由REL(可重定位目标文件)变为EXEC(可执行目标文件),Entry point address即程序入口点由0x0(未确定)变为了0x4010f0,程序和节的起始位置和大小也均有改变,节个数由14个变为31个。

 

图5-2 可执行目标文件的ELF头

5.3.2 节头部表

节头描述了各个节的大小、偏移量和其他属性。链接器链接时,会将各个文件的相同段合并成一个大段,并且根据这个大段的大小以及偏移量重新设置各个符号的地址。由图5-3的节头信息可以看出,第一个特点就是节的数目有了显著增加,由14个变为31个,增加了许多具有其他作用的节,这里就不一一赘述了。

 

图5-3 可执行目标文件的节头部表

5.3.3 符号表

可执行文件的符号表(图5-5)中多了很多符号,而且额外有一张动态符号表(.dynsym),如图5-4所示。printf puts exit getchar等C标准库函数在动态符号表和符号表中都有表项。此外一个与可重定向目标文件的不同是,这些符号已经确定好了运行时位置。

图5-4 动态符号表

 

图5-5 符号表

5.4 hello的虚拟地址空间

使用edb加载hello,查看本进程的虚拟地址空间各段信息。

通过查看edb,看出hello的虚拟地址空间开始于0x401000,结束于0x402000。

 

图5-6 Linux进程的虚拟地址空间布局

查看 ELF 格式文件中的 Program Headers,它告诉链接器运行时加载的内容,并提供动态链接的信息。每一个表项提供了各段在虚拟地址空间和物理地址空间的各方面的信息。根据5.3.2的节头部表的信息,可以通过edb找到各个节的信息,比如.txt节虚拟地址开始于0x4010f0,可以在Data Dump中寻找其位置如图5-6所示。

 

图5-7 .text的位置

5.5 链接的重定位过程分析

命令:objdump -d -r hello

分析hello与hello.o的不同:

(1) 地址访问。hello.o中的相对偏移地址变成了hello中的虚拟内存地址。而hello.o文件中对于某些地址的定位是不明确的,其地址也是在运行时确定的,因此访问也需要重定位,在汇编成机器语言时,将操作数全部置为0,并且添加重定位条目。hello反汇编的代码有确定的虚拟地址,也就是说已经完成了重定位。

(2) hello反汇编的代码中多了很多的节以及很多函数的汇编代码,增加了.init和.plt节,和一些节中定义的函数。图5-8展示了部分新增的节,这些节都具有一定的功能和含义,在5.3节已做过陈述。

图5-8 新增节的部分截图

(3) 函数调用。hello中无hello.o中的重定位条目,并且跳转和函数调用的地址在hello中都变成了虚拟内存地址。对于hello.o的反汇编代码,函数只有在链接之后才能确定运行执行的地址,因此在.rela.text节中为其添加了重定位条目。

链接过程:

根据hello和hello.o的不同,分析出链接就是链接器(ld)将各个目标文件(各种.o文件)组装在一起,文件中的各个函数段按照一定规则累积在一起。

(1) 符号解析:将每个符号引用正好和一个符号定义关联起来;

(2) 重定位:所有相同类型的节合并成同一类型的新的聚合节,链接器将运行时内存地址付赋给每个节、每个符号,此时程序中每条指令、每个符号都有唯一的运行时内存地址了;再依据重定位条目,修改对每个符号的引用。

此重定位条目会告诉链接器修改偏移量为0x1b处的相对引用。此时链接器已知main的地址为0x4010c1,puts函数的地址为0x401030,则引用处地址为0x4010c1+0x1b=0x4010dc,相对偏移为:puts的地址+addend-引用处地址,即0x401030-0x4-0x4010dc=-0xb0,转换成补码表示就是ff ff ff 50,小端模式填入即可。绝对寻址极其简单,直接填入地址即可。

5.6 hello的执行流程

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

加载hello到_start之间调用_dl_start和_dl_init函数;main函数之前会调用_init函数初始化代码;main调用一些库函数如puts;程序终止调用exit函数。

加载hello:_dl_start、_dl_init;

main函数之前:_start、__libc_start_main、__cxa_aexit、_init、_setjmp、_sigsetjmp;

main函数:main、puts、exit、printf、sleep、getchar;

main函数之后:exit。

5.7 Hello的动态链接分析

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

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

动态链接会把库函数的文本和代码加载进来,hello程序是在加载时进行动态链接。在加载,_dl_init之前,文件中并没有库函数的代码、数据。_dl_init之后,库函数的代码、数据被加载进文件。

由图5-9得,.got起始表的位置是0x404000。

图5-9 hello的节头部表

使用edb查看Data Dump,发现GOT表位置在调用dl_init之前0x601008后的16个字节均为0。

 

图5-10 edb运行init之前.got节的内容

点击edb的运行按钮后,可以看到之前空白的字节变成了0x7f6a98c54190和0x7f6a98c3dbb0,其中GOT[0]~GOT[2]是动态链接器相关的地址,GOT[3]开始为调用函数的地址,而每个表项为8字节。在symbol viewer中查找该地址,发现正好就是puts在内存中的位置。依次可以验证后面的几项是printf、getchar等。这说明在hello的_start开始前调用了动态链接的相关函数,修改GOT表的内容,将其指向库中函数的运行时位置。

 

图5-11 edb运行程序之后.got的内容

5.8 本章小结

本章介绍链接阶段,介绍了链接的概念、命令行,介绍了可执行目标文件的ELF格式和各个节的含义,分析了hello的虚拟地址空间、各段的地址,详细介绍了链接的重定位过程,分析了hello的从加载到终止的流程,分析了动态链接前后哪些项目发生了变化。

6章 hello进程管理

6.1 进程的概念与作用

进程就是一个执行中程序的实例。这是一种非正式的说法。进程不只是程序代码,还包括当前活动,如PC、寄存器的内容、堆栈、数据段,还可能包含堆[2]。

进程提供了两个关键的抽象:1、独立的逻辑控制流,好像程序在独占地使用处理器和内存;2、私有的地址空间,处理器好像是无间断的执行我们程序中的指令,好像程序独占的使用内存。

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

Linux系统中,Shell是一个交互型应用级程序,代表用户运行其他程序,解释并运行用户的指令,连接用户和操作系统以及内核。

重复如下处理过程:

(1) 终端进程读取用户由键盘输入的命令行。

(2) 调用parseline函数,分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量。

(3) 调用builtin_command函数,检查第一个命令行参数是否是一个内置的shell命令,如果是则立刻解释这个命令。

(4) 如果不是内部命令,调用fork( )创建新的子进程。

(5) 在子进程中,用步骤2获取的参数,调用execve( )执行指定程序。

(6) 如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid(或wait)等待作业终止后返回。

6.3 Hello的fork进程创建过程

shell解析命令行,发现第一个参数是./hello而不是内置命令,就调用fork创建子进程,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。

fork创建一个子进程,子进程与父进程除了PID几乎完全相同。fork函数只会被调用一次,但会返回两次,在父进程中返回子进程的pid,子进程中返回0;子进程与父进程有完全相同的虚拟地址空间,但是是独立的,对一个进程私有的变量做的改变不会反映到另一个进程之中;子进程与父进程并发执行;子进程继承父进程打开的文件。

6.4 Hello的execve过程

在shell为hello创建子进程后,shell调用execve函数。execve在当前进程的上下文中加载并运行一个新的程序。步骤如下所示:

(1) execve调用启动加载器,加载器删除子进程现有的虚拟内存段。

(2) 映射私有区:创建一组新的代码、数据、堆栈段,新的代码、堆栈段被初始化为可执行文件的内容,堆栈段被初始化为0。

(3) 映射共享区:比如Hello程序与标准C库libc.so链接,这些对象都是动态链接到程序的,然后再映射到用户虚拟地址空间中的共享区域内。

(4) 设置程序计数器PC:exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。下一次调用这个进程时,它将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。

6.5 Hello的进程执行

进程时间片:一个进程执行它的控制流的一部分的每一时间段。

上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构等对象的值构成。

图6-1 上下文切换示例

用户模式和内核模式:处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,进程就处于用户模式中,用户模式的进程不允许执行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;设置模式位时,进程处于内核模式,该进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。

调度的过程:当进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策叫做调度。通过上下文切换的机制来转移控制:1、保存当前进程的上下文;2、回复即将调度的进程的上下文;3、将控制转移给被调度的进程。

以hello.c为例,hello并不是操作系统中运行的唯一进程。为了最大化CPU利用率,需要进行进程调度。当一个进程等待时,操作系统从该进程接管CPU控制,并将CPU交给另一进程。进行上下文切换时,陷入内核态,内核会将旧进程状态保存在其进程管理块中,然后加载经调度而要执行的新进程的上下文,并将控制重新转移给用户态。上下文切换的典型速度为几毫秒。

6.6 hello的异常与信号处理

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

6.6.1 异常

异常可以分为四类:中断、陷阱、故障、终止,各自的属性如表6-1所示。

表6-1 异常类型

hello程序出现的异常可能有:

中断:在hello程序执行的过程中可能会出现外部I/O设备引起的异常。

陷阱:陷阱是有意的异常,是执行一条指令的结果,hello执行sleep函数的时候会出现这个异常。

故障:在执行hello程序的时候,可能会发生缺页故障。

终止:终止时不可恢复的错误,在hello执行过程可能会出现DRAM或者SRAM位损坏的奇偶错误。

6.6.2 信号以及处理

当进程收到一个信号且该信号未被阻塞,该进程就会调用异常处理程序来处理该信号。如果未用signal函数指定信号处理程序,该进程会执行默认行为。否则,进程的控制会被内核转移到该信号处理程序处。

hello程序可能发出的信号:

1、SIGINT/SIGTSTP

当hello执行过程中用户在键盘上按下Ctrl+C/Z,会使得操作系统给所有前台进程(此时仅有shell)发送一个SIGINT/SIGTSTP信号。当shell捕获到这个SIGINT/SIGTSTP信号,它检查前台进程组并给hello进程发送(通过kill函数)一个SIGINT/SIGTSTP,由于hello没有设置对于这两种信号的处理程序,它执行默认操作:终止/停止,并向父进程shell发送一个SIGCHLD信号。

2、SIGCHID

当hello向shell发送一个SIGCHLD,shell会调用自己的SIGCHLD处理程序来回收子进程(通过waitpid函数)。shell需要获取子进程的退出状态(用waitpid中修改过的status参数),并且根据其退出状态做不同的处理:当子进程停止,shell仅仅是将它的运行状态改为停止;否则,shell直接将其回收,并视情况输出提示信息。

6.6.3 实际测试

hello程序正常运行的结果如图6-2所示。

图6-2 hello正常运行截图

对正在运行的hello用Ctrl-z发送SIFTSTP信号,然后用ps和jobs命令查看进程和作业。

图6-3 Ctrl-z暂停程序运行

用fg命令使hello恢复前台运行,再用Ctrl-c发送SIGINT信号终止进程,可以看到已经没有作业了。

图6-4 fg命令和Ctrl-c终止程序

用pstree命令查看进程树。

图6-5 pstree查看进程树

用kill命令向进程发送SIGKILL信号。

图6-6 kill命令杀死进程

程序运行的时候夹杂着空格乱按键盘,由图6-6可见,程序运行的过程中乱按就是输入到输入缓冲区,第一次回车之前的内容会让getchar读走,之后所有的输入都留在输入缓冲区里,之后如果再有回车的话就会当成是之后的shell命令行了。

图6-7 夹杂着空格乱按

6.7本章小结

本章介绍了进程的概念与作用,介绍了shell的处理流程和作用,着重分析了fork、execve执行hello的过程,上下文切换的相关内容,以及异常与信号处理的相关内容。

7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址:程序经过编译后出现在汇编代码中的地址。逻辑地址用来指定一个操作数或者是一条指令的地址。是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 [段标识符:段内偏移量]。

线性地址:和逻辑地址类似,也是一个不真实的地址,如果逻辑地址是对应的硬件平台段式管理转换前地址的话,那么线性地址则对应了硬件也是内存的转换前地址。在IA 32中,线性地址由逻辑地址经过转换得到。先用段选择符到全局描述符表中取得段基址,再加上段内偏移量,即得到线性地址。IA 64中,由于不存在段,偏移量也就不需要转换才能得到线性地址了,因此逻辑地址==线性地址。

虚拟地址:虚拟地址强调程序拿到的地址并不是真实的物理地址,而是一个虚拟的地址(由逻辑地址表示),需要经过到线性地址再到物理地址的变换。有时我们也把逻辑地址称为虚拟地址。因为与虚拟内存空间的概念类似,逻辑地址也是与实际物理内存容量无关的,是hello中的虚拟地址。

物理地址:用于内存芯片级的单元寻址,与处理器和CPU链接的地址总线相对应。可以直接把物理地址理解成插在机器上那根内存本身,把内存看成一个从0字节一直到最大空量逐字节的编号的大数组,然后把这个数组叫做物理地址,但是事实上,这只是一个硬件提供给软件的抽像,内存的寻址方式并不是这样。所以,说它是“与地址总线相对应”,是更贴切一些,不过抛开对物理内存寻址方式的考虑,直接把物理地址与物理的内存一一对应,也是可以接受的。也许错误的理解更利于形而上的抽象。

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

段式管理是应用在IA32架构上的管理模式。一组寄存器(CS,DS,SS等)保存着当前进程各段(如代码段、数据段、堆栈段)在描述符表中的索引,可以用来查询每段的逻辑地址。一个逻辑地址由两部分组成,段标识符,段内偏移量。段标识符是一个16位长的字段组成,称为段选择符,其中前13位是一个索引号。后面三位包含一些硬件细节。

索引号就是“段描述符(segment descriptor)”的索引,段描述符具体地址描述了一个段。很多个段描述符,就组了一个数组,叫“段描述符表”,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。

Base字段描述了一个段的开始位置的线性地址。全局的段描述符,放在“全局段描述符表(GDT)”中,一些局部的段描述符,放在“局部段描述符表(LDT)”中。GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。给定一个完整的逻辑地址段选择符+段内偏移地址,看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。

拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。把Base + offset,就是要转换的线性地址了

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

无论是IA32的段式管理还是页式管理,都需要查询页表将线性地址转换为物理地址。线性地址被分为数个部分。MMU内存管理单元取得线性地址的前面一部分并以其为索引(虚拟页号)查询页表的表项,得到物理页号,再将物理页号与线性地址的后面一部分拼接到一起,得到物理地址,CPU就可以通过这个物理地址访问到内存。

地址变换的步骤如下:

1、将线性地址传给MMU,MMU根据虚拟页号计算页表项地址;

2、MMU请求得到页表项,得到页表项中的物理页号;

3、将物理页号和线性地址中的页偏移串联得到物理地址。

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

每次CPU产生一个虚拟地址,MMU(内存管理单元)就必须查阅一个PTE(页表条目),以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会从内存多取一次数据,代价是几十到几百个周期。如果PTE碰巧缓存在L1中,那么开销就会下降1或2个周期。然而,许多系统都试图消除即使是这样的开销,它们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓存器(TLB)。

1、使用VPN在TLB中查找页表项,若TLB命中,取出页表项中的PPN,再把PPN和VPO串联起来得到物理地址;

2、若TLB不命中,使用四级页表查找页表项,CR3寄存器中存放一级页表的物理地址,VPN被分成四块,VPN1是在一级页表的偏移,这个一级页表中的PTE包含一个二级页表的基址,VPN2是在二级页表的偏移,以此类推,最终得到一个PTE,若PTE有效位为1,则取出PPN,和VPO串联得到物理地址;若PTE有效位为0,则调用缺页异常处理子程序。

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

CPU发送一条虚拟地址,随后MMU按照上述操作获得了物理地址PA。根据cache大小组数的要求,将PA分为CT(标记位)CS(组号),CO(偏移量)。根据CS寻找到正确的组,比较每一个cacheline是否标记位有效以及CT是否相等。如果命中就直接返回想要的数据,如果不命中,就依次去L2,L3,主存判断是否命中,当命中时,将数据传给CPU同时更新各级cache的cacheline(如果cache已满则要采用换入换出策略)。

缓存以块为基本单元进行,也就是对于一个单元的请求会把它附近的一个特定大小的块也缓存到更高层的存储器。这依赖于程序拥有的空间局部性和时间局部性。空间局部性指一个被引用的内存单元附近的内存很有可能在不久后被引用;时间局部性指一个被引用的内存单元很有可能在不久后被多次引用。因此,一个具有良好局部性的程序会减少花费在访问内存上的时间:一次缓存可以供后来的多次内存访问使用。因此,程序员应该根据具体问题对程序的写法进行适当的调整,以利用缓存提供的性能提升。

7.6 hello进程fork时的内存映射

当shell调用fork时,分配给子进程唯一的一个PID,内核为新进程创建包括页表、区域结构、mm_struct等数据结构,并将新进程与父进程映射到同一块虚拟内存,并标记这些页为只读,将两个进程中的每个区域结构都标记为私有的写时复制。通过这种方法,如果父子进程仅仅是读某一块内存,它们不需要花费额外时间来创建一块多余的副本;当其中某个进程需要写某区域时,这个写操作会触发一个保护故障,从而导致故障处理程序在物理内存中创建这个页面的一个新副本,并将页表条目指向这个新副本,恢复这个页面的可写权限。私有的写时复制节省了创建页面的时间,并充分利用了物理内存。

7.7 hello进程execve时的内存映射

execve 函数调用驻留在内核区域的启动加载器代码,在当前进程中加载并运 行包含在可执行目标文件 hello 中的程序,用 hello 程序有效地替代了当前程序。 加载并运行 hello 需要以下几个步骤:

(1) 删除已存在的用户区域,删除当前进程虚拟地址的用户部分中的已存在的区域结构(即除了内核以外的部分,包括代码、数据、bss、堆、栈、共享库内存映射区域)。

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

(3) 映射共享区域, hello 程序与共享对象 libc.so 链接,libc.so 是动态链 接到这个程序中的,然后再映射到用户虚拟地址空间中的共享区域内。

(4) 设置程序计数器(PC),execve 做的最后一件事情就是设置当前进程上下文的程序计数器,设置rip寄存器到代码区域的入口点,使之指向代码区域的入口点。

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

页面命中完全是由硬件完成的,而处理缺页是由硬件和操作系统内核协作完成的。当指令引用一个相应的虚拟地址,而与改地址相应的物理页面不再内存中,会触发缺页故障。通过查询页表PTE可以知道虚拟页在磁盘的位置。缺页处理程序从指定的位置加载页面 到物理内存中,并更新PTE。然后控制返回给引起缺页故障的指令。当指令再次执行时,相应的物理页面已经驻留在内存中,因此指令可以没有故障的运行完成。

整体的处理流程:

(1) 处理器生成一个虚拟地址,并将它传送给MMU。

(2) MMU生成PTE地址,并从高速缓存/主存请求得到它。

(3) 高速缓存/主存向MMU返回PTE。

(4) PTE中的有效位是0,所以MMU出发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序。

(5) 缺页处理程序确认出物理内存中的牺牲页,如果这个页已经被修改了,则把它换到磁盘。

(6) 缺页处理程序页面调入新的页面,并更新内存中的PTE

(7) 缺页处理程序返回到原来的进程,再次执行导致缺页的命令。CPU将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面已经换存在物理内存中,所以就会命中。

7.9动态存储分配管理

动态储存分配管理使用动态内存分配器来进行。动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可以用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配的状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。动态内存分配主要有两种基本方法与策略:

带边界标签的隐式空闲链表分配器管理

带边界标记的隐式空闲链表的每个块是由一个字的头部、有效载荷、可能的额外填充以及一个字的尾部组成的。

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

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

显示空间链表管理

显式空闲链表是将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。如,堆可以组织成一个双向链表,在每个空闲块中,都包含一个前驱与一个后继指针。

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

放置已分配块的策略

首次适配:从头开始搜索链表,选择第一个合适的空闲块;

下一次适配:每次从上一次查询结束的位置开始搜索;

最佳适配:搜索每个空闲块,选择适合所需请求大小的最小空闲块。

7.10本章小结

本章主要介绍了hello的存储器地址空间、段式管理及页式管理,即逻辑、线性、物理地址的转换,介绍了页表的缓存——TLB、多级页表,并详细的描述了TLB与四级页表支持下VA到PA的转换,介绍了物理内存访问,还介绍了虚拟内存机制下fork、execve的实质,介绍缺页故障与缺页中断处理的具体行为,最后概述了动态存储分配管理。

8章 hello的IO管理

8.1 Linux的IO设备管理方法

一个Linux文件就是一个m字节的序列。所有的 IO 设备(如网路、磁盘、终端)都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单低级的应用接口,称为 Unix I/O,这使得所有的输入和输出都以统一的方式被当做相应文件的读和写来执行。

设备的模型化:文件

设备管理:unix io接口

8.2 简述Unix IO接口及其函数

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

8.2.1 Unix IO接口

(1) 打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个 I/O 设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。

(2) Shell 创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误。

(3) 改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置 k,初始为 0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行 seek,显式地将改变当前文件位置 k。

(4) 读写文件:一个读操作就是从文件复制 n>0 个字节到内存,从当前文件位置 k 开始,然后将 k 增加到 k+n,给定一个大小为 m 字节的而文件,当 k>=m 时,触发 EOF。类似一个写操作就是从内存中复制 n>0 个字节到一个文件,从当前文件位置 k 开始,然后更新 k。

(5)关闭文件,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。

8.2.2 Unix IO函数

1、open

进程通过调用open函数来打开一个已存在的文件或创建一个新文件。函数原型:int open(char *filename, int flags, mode_t mode);

open函数将filename转换为一个文件描述符(用数字表示)。返回的数字总是进程中没有打开的最小描述符。flags指明了打开该文件的方式,mode给该文件赋予更多的权限选项,指定了新文件的访问权限位。

2、close

int close(fd),fd 是需要关闭的文件的描述符,close 返回操作结果。

3、read & write

进程通过调用read和write函数来执行输入输出。函数原型:

ssize_t read(int fd, void *buf, size_t n);

ssize_t write(int fd, void *buf, size_t n);

read函数从描述符为fd的文件复制最多n个字节到内存位置buf。函数返回-1说明遇到错误,返回0表示EOF。否则,返回读取的字节数量。

write函数从内存位置buf处至多复制n个字节到文件描述符为fd的文件中,若函数返回-1说明遇到错误,否则返回实际写入的字符数量。

8.3 printf的实现分析

printf的源码如图8-1所示[3]。

图8-1 printf函数的函数体

printf程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。

接下来是write函数如图8-2所示。

图8-2 write函数源码

在printf中调用系统函数write(buf,i)将长度为i的buf输出,在write函数中,将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址,

int INT_VECTOR_SYS_CALLA代表通过系统调用syscall。

syscall函数体如图8-3所示。

图8-3 syscall函数源码 

syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。

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

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

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

于是我们的打印字符串就显示在了屏幕上。

8.4 getchar的实现分析

异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键 的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子 程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码 转换成 ASCII 码,保存到系统的键盘缓冲区。

getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。getchar 进行封装,大体逻辑是读取字符串的第一个字符然后返回。

当程序调用getchar(),程序等待用户输入。用户输入的每个字符实际上是一个中断,其触发事件为键盘按下,行为是将按下的对应字符保存到输入缓冲区。当按下的字符为回车时,中断处理程序将结束getchar并返回读入的第一个字符。

8.5本章小结

本章主要介绍了Linux的I/O设备管理方法,简述了Unix I/O接口及其函数,分析了printf函数和getchar函数的具体实现和printf函数的源代码。

结论

用计算机系统的语言描述hello的一生:

1、编写源文件,编程,通过键盘输入,创建hello.c;

2、预处理阶段,预处理器扩展源代码,将hello.c调用的所有外部的库展开合并到一个hello.i文件中。

3、编译阶段,将hello.i编译成为汇编文件hello.s;

4、汇编阶段,将hello.s汇编成为二进制可重定位目标文件hello.o;

5、链接阶段,将hello.o与可重定位目标文件和动态链接库链接成为可执行目标程序hello;

6、运行程序:在shell中输入./hello 120L021303 苏泓斌 1;

7、创建子进程并加载:shell解析命令行,然后调用fork和execve函数创建子进程并在子进程中加载运行hello程序;

8、信号:如果运行途中键入ctr-c ctr-z则调用shell的信号处理函数分别停止、挂起;

9、执行指令:CPU为其分配时间片,进程、虚拟空间给hello提供两个假象:好像在独占的使用处理器,好像在独占的使用内存;

10、动态申请内存:printf会调用malloc向动态内存分配器申请堆中的内存;

11、圆满的谢幕:运行结束,shell回收子进程,操作系统内核删除相关的数据结构。

自此,一个hello程序落幕, CSAPP这门课程也缓缓谢幕。CSAPP贵为计算机基础书籍顶级之作,介绍了计算机系统的基本概念,包括最底层的内存中的数据表示、流水线指令的构成、虚拟存储器、编译系统、动态加载库,以及用户应用等。经过一学期对计算机系统的学习,我认识到计算机系统是一个经过无数人的思考,设计出来的一个无比精妙的整体。这门课让我们认识到计算机隐藏在抽象下的运作方式,指导我们写出更有效率、更加安全的代码;告诉我们遇到计算机相关的问题时应该如何思考并解决问题。无论计算机在未来有怎样的变化,这门课程也不会失去其价值。hello,你曾经来过!

附件

文件

作用

hello.i

hello.c预处理之后的文本文件

hello.s

hello.i编译后的汇编文件,文本文件

hello.o

hello.s汇编之后的可重定位目标文件,二进制文件

hello

链接之后的可执行目标文件,二进制文件

hello.out

hello反汇编之后的可重定位文件

参考文献

[1]  RandalE.Bryant, DavidO'Hallaron. Computer System: a Programmer's Perspective: 第2版[M]. 机械工业出版社, 2016.9.

[2]  西尔伯斯查兹, 高尔文, 加尼,等. 操作系统概念[M]. 高等教育出版社, 2004.

[3]  [转]printf 函数实现的深入剖析 - Pianistx - 博客园 (cnblogs.com).

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值