摘 要

本文通过对hello程序在Ubuntu系统下从程序变成进程再被回收的过程的分析,深入剖析了程序在计算机系统中的运行流程。我们能在这个过程中,看到hello程序一步一步从普通的c语言代码,成长为可执行的文件;再破茧成蝶,由一个可执行的文件蜕变成系统中的进程,让shell为它创建进程,再在进程中与计算机交互,调用函数的整个过程;与此同时,我们也能在计算机中交互梳理复习《深入学习计算机系统》的主要内容,让我们对计算机系统的理解更加深刻。

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

目录

    • 摘 要
  • 第1章 概述
    • 1.1hello简介
    • 1.2 环境与工具
    • 1.3 中间结果
    • 1.4 本章小结
  • 第2章 预处理
    • 2.1 预处理的概念与作用
    • 2.2在Ubuntu下预处理的命令
    • 2.3 Hello的预处理结果解析
    • 2.4 本章小结
  • 第3章 编译
    • 3.1 编译的概念与作用
    • 3.2 在Ubuntu下编译的命令
    • 3.3 Hello的编译结果解析
      • 3.3.1 数据
        • 3.3.1.1 常量
        • 3.3.1.2 变量
      • 3.3.2赋值操作
      • 3.3.3 类型转换
      • 3.3.4 算术操作
      • 3.3.5 关系操作
      • 3.3.6 数组操作
      • 3.3.7控制转移
      • 3.3.8 函数操作
        • 3.3.8.1 main函数
        • 3.3.8.2 printf函数
        • 3.3.8.3 exit函数
        • 3.3.8.4 atoi函数
        • 3.3.8.5 sleep函数
        • 3.3.8.6 getchar函数
    • 3.4 本章小结
  • 第4章 汇编
    • 4.1 汇编的概念与作用
    • 4.2 在Ubuntu下汇编的命令
    • 4.3 可重定位目标elf格式
      • 4.3.1 命令
      • 4.3.2 ELF头
      • 4.3.2 节头部表
      • 4.3.3 重定位节
      • 4.3.4 符号表
    • 4.4 Hello.o的结果解析
    • 4.5 本章小结
  • 第5章 链接
    • 5.1 链接的概念与作用
    • 5.2 在Ubuntu下链接的命令
    • 5.3 可执行目标文件hello的格式
    • 5.4 hello的虚拟地址空间
    • 5.5 链接的重定位过程分析
    • 5.6 hello的执行流程
    • 5.7 Hello的动态链接分析
    • 5.8 本章小结
  • 第6章 hello进程管理
    • 6.1 进程的概念与作用
    • 6.2 简述壳Shell-bash的作用与处理流程
    • 6.3 Hello的fork进程创建过程
    • 6.4 Hello的execve过程
    • 6.5 Hello的进程执行
      • 6.5.1 逻辑控制流
      • 6.5.2 进程时间片与并发流
      • 6.5.3 内核模式和用户模式
      • 6.5.5 hello的执行过程
    • 6.6 hello的异常与信号处理
      • 6.6.1 hello的异常
      • 6.6.2 hello信号
      • 6.6.3 hello命令和运行结果截屏
    • 6.7本章小结
  • 第7章 hello的存储管理
    • 7.1 hello的存储器地址空间
    • 7.2 Intel逻辑地址到线性地址的变换-段式管理
    • 7.3 Hello的线性地址到物理地址的变换-页式管理
    • 7.4 TLB与四级页表支持下的VA到PA的变换
      • 7.4.1 TLB
      • 7.4.2 四级页表
    • 7.5 三级Cache支持下的物理内存访问
    • 7.6 hello进程fork时的内存映射
    • 7.7 hello进程execve时的内存映射
    • 7.8 缺页故障与缺页中断处理
    • 7.9动态存储分配管理
    • 7.10本章小结
  • 第8章 hello的IO管理
    • 8.1 Linux的IO设备管理方法
    • 8.2 简述Unix IO接口及其函数
    • 8.3 printf的实现分析
    • 8.4 getchar的实现分析
    • 8.5本章小结
  • 结论
  • 附件
  • 参考文献

第1章 概述

1.1hello简介

Hello的P2P过程:
P2P(From Program to Process)指hello.c程序由程序变成进程的过程。用户编写的hello.c程序,是为program;在linux系统下,hello.c可通过编译器驱动程序,经历预处理器、编译、汇编、链接四个过程,得到可执行文件hello。在系统中运行hello程序,系统会用fork创建hello进程,是为process。而这就是P2P。
Hello的020过程:
020(From Zero-0 to Zero-0)指hello.c程序进程先生成、再被回收的过程。shell用fork创建hello进程,生成了进程;再调用execve映射虚拟内存、载入物理内存。进入main函数后,CPU为程序分配时间片、执行控制流,cache、TLB等加速程序的运行。当程序运行结束后,shell回收hello进程,删除在内核中有关hello的所有数据,进程被回收,这就是020。

1.2 环境与工具

硬件环境:
Intel® Core™ i7-10750H X64 CPU @2.60GHz
8GB RAM
512G SSD
软件环境:
Windows 11 21H2
Ubuntu 20.04 LTS
开发工具:
CodeBlocks20.03;vi/vim/gpedit+gcc;Visual Studio 2010 64位以上;GDB/EDB,readelf等;

1.3 中间结果

文件名 函数作用
hello.i hello.c经过预处理后的文件
hello.s hello.i经过编译后的汇编文件
hello.o hello.s经过汇编后的可重定位目标文件
hello hello.o经过链接之后的可执行目标文件
hello-elf hello.o的ELF格式文件
hello-dis hello.o 经过反汇编获得的反汇编文件
hello-elf2 hello的ELF格式文件
hello-dis2 hello 经过反汇编获得的反汇编文件

1.4 本章小结

本章我们首先介绍了hello的P2P和020过程,然后说明了实验所用到的硬件环境、软件环境和工具,最后叙述了实验中产生的中间结果和作用。

第2章 预处理

2.1 预处理的概念与作用

预处理的概念:
当对一个源文件进行编译时,预处理器cpp根据以字符#开头的命令(如文件包含(#include)等),修改原始的C程序,将引用的所有库展开合并成为一个完整的文本文件,并删除程序中的注释和多余空白字符,产生新代码提供给编译器,这就是预处理的过程。
预处理的作用:
预处理主要与三部分有关:
宏定义,用文本替换宏名;
文件包含,读取#include部分,将头文件和当前源文件连成一个源文件;
条件编译,根据#if/#endif/#ifdef/#ifndef等判断执行编译的条件。

2.2在Ubuntu下预处理的命令

预处理命令:gcc -m64 -no-pie -fno-PIC -E hello.c -o hello.i
代码及生成代码文件图片如下:

图2.1 在Ubuntu下程序预处理的命令及获取文件截图

2.3 Hello的预处理结果解析

可以看到,程序从23行变成了3060行。这3060行的最后15行是hello.c的程序,这程序中没有注释,如图所示:

图2.2 产物中的程序没有注释

前面3042行是对hello.c引用的3个头文件<stdio.h>、<unistd.h> 、<stdlib.h>的展开。
展开部分包含代码文件的位置,如下图所示:

图2.3 头文件的代码位置

还包含了一些枚举、类型的定义、结构体、通过exturn调用外部结构体和函数的定义,如图所示:

图2.4 定义结构体等内容

同时,这些头文件中还包含了宏定义、条件编译等内容,并且删掉了注释,然后就获得了hello.i。

2.4 本章小结

本章介绍了程序预处理的概念及作用,并对hello.c程序做了预处理,分析了获得的产物文件hello.i的内容以及过程,比对了hello.c和hello.i的区别。我们了解到了预处理器做的事如下:展开宏定义、文件包含、条件编并删除注释。我们的hello.c文件变成了hello.o,正式开启了自己的成长之旅。

第3章 编译

3.1 编译的概念与作用

编译的概念:
编译是利用编译程序从源语言编写的源程序,经过词法分析、语法分析、语义分析以及经过一系列优化,生成汇编代码产生目标程序的过程。
编译的作用:
编译的作用在于将便于人读写的高级计算机语言的源程序转换为机器可识别的低阶机器语言程序。除此之外,编译程序还有语法检查、调试措施、修改手段、覆盖处理、目标程序优化和不同语言合用等功能。

3.2 在Ubuntu下编译的命令

编译的命令:gcc -m64 -no-pie -fno-PIC -S hello.i -o hello.s

图3.1 在Ubuntu下编译的命令及获取文件截图

3.3 Hello的编译结果解析

首先,第一部分使用了一些指令,如.file等,这些指令的含义如下:

图3.2 指令的内容及含义

3.3.1 数据

3.3.1.1 常量

源代码有if(argc!=4)、exit(1)、for(i=0;i<8;i++)等,这几处出现的4、1、0、8是常量,作为立即数存储在.test部分中,如图所示:

图3.3 4作为立即数存储在.test中

源代码有printf(“Hello %s %s\n”,argv[1],argv[2])、printf(“用法: Hello 学号 姓名 秒数!\n”)其中字符串"Hello %s %s\n"、"用法: Hello 学号 姓名 秒数!\n"为字符串常量,储存在.rodata部分中第六、第八行,如图所示:

图3.4 字符串存在.rodata中

3.3.1.2 变量

源代码中没有全局变量,初始化的全局变量应存储在.data段中,未初始化的全局变量应存储在.bss段中。
源代码中存在局部变量 int i、char argc、char *argv[]等,局部变量存储在寄存器或栈中。程序中的i存在栈中,如图所示:

图3.5 局部变量i

显示该变量存在-4(%rbp)中。i是int型变量,占用4个字节,因此此处用了movl去移动。
argc是字符,argv是字符串数组,两个作为main函数的参数由寄存器传入,进而保存在堆栈中。

图3.6 传入argc和argv,分别保存在-20(%rbp)和-32(%rbp)中

其中数组在调用时,用寄存器寻址的方法访问。

图3.7 寄存器寻址访问

3.3.2赋值操作

源代码中for(i=0;i<8;i++)中的i=0是赋值操作,此操作对应的汇编如下:

图3.8 循环开始,将立即数0赋给i

3.3.3 类型转换

源代码中sleep(atoi(argv[3]))中的atoi(argv[3])是类型转换,它将argv[3]中的字符串转换为int型,并作为参数传到sleep中。对应汇编如下:

图3.9 字符串型转换为int型并传回

3.3.4 算术操作

源代码中存在算术操作,是for(i=0;i<8;i++)中的i++操作,此操作对应的汇编如下:

图3.10 i++操作对应的代码段

段中51行addl $1,-4(%rbp)代表i++操作。

3.3.5 关系操作

源代码中存在关系操作,有两处,一处是if(argc!=4)有一不等于的条件操作,体现在汇编代码中如下:

图3.11 不等于操作

此处汇编用cmpl将4与存在-20(%rbp)的argc作比较,等于则跳转。
另一处是for(i=0;i<8;i++)中的i<8操作,是一处小于的操作,在汇编中对应如下:

图3.12 小于操作

此处汇编用cmpl将7与存在-4(%rbp)的i作比较,大于等于7则跳转。

3.3.6 数组操作

源代码中出现了数组char *argv[]。这个数组作为参数被传入主函数中,其存储位置和调用方法在3.3.1.2早已分析过。在函数中对该数组的操作有两处,均为引用操作。
第一处在printf(“Hello %s %s\n”,argv[1],argv[2])处,引用了argv[1]和argv[2],对应汇编语言如下:

图3.13 数组引用1

此处汇编首先把数组头的地址存入rax中,再将rax中的地址+16(即(%rbp)-16),找到了argv[2]的位置,并将此地址存入rdx中,以便作为函数参数调用。同理,在(%rbp)-24的argv[1]存入了rsi中,构成printf函数的两个参数。
第二处在sleep(atoi(argv[3])),此处引用了argv[3],对应汇编语言如下:

图3.14 数组引用2

同理,rax地址+24(即(%rbp)-8)找到argv[3]位置存入rdi传参。

3.3.7控制转移

程序中共出现了两次控制转移,分别是if(argc!=4)中的if语句和for(i=0;i<8;i++)中的for循环。if语句的控制转移如下:

图3.15 if语句的控制转移

此处汇编将4与存在-20(%rbp)的argc作比较,如果相等就跳转到.L2循环开始处,不等则不跳转而执行exit等函数。
for语句的控制转移如下:

图3.16 for语句的控制转移

循环开始部分位于L3,将i与7比较,如果小于等于7则返回跳转到.L2处执行循环内内容,执行完后给i+1,来到L3的继续比较,以此类推,i得8时则不跳转执行后面的语句。

3.3.8 函数操作

源代码中调用了main、printf、exit、atoi、sleep、getchar六个函数。函数的调用在汇编中用call表示。

3.3.8.1 main函数

参数传递:函数共两个参数,分别为int argc、char *argv[],在3.3.1.2分析得知两个参数分别存在20(%rbp)和-32(%rbp)中。
函数调用:main函数由系统启动程序调用。
局部变量:在3.3.1.2有分析。
函数返回:原函数中返回值为return 0,对应汇编如下:

图3.17 for语句的控制转移

可以看到,汇编语言中先把0赋给eax,让程序能够返回0;然后调用leave指令(leave相当于mov %rbp,%rsp和pop %rbp),恢复栈空间为调用之前的状态,然后ret返回。

3.3.8.2 printf函数

参数传递:函数第14行的printf(“用法: Hello 学号 姓名 秒数!\n”)参数为字符串"用法: Hello 学号 姓名 秒数!\n",此字符串在函数头初始化在.LC0部分。第18行的printf(“Hello %s %s\n”,argv[1],argv[2])传入了三个参数,分别为字符串"Hello %s %s\n"和argv[1]、argv[2],第一个字符串在函数头初始化在.LC1部分。

图3.18 初始化过程

后面两个字符串位置已在3.3.5分析,存在(%rbp)-24和(%rbp)-16中。
函数调用:第一处在汇编语言中被调用过程如图所示:

图3.19 调用过程1

将.LC0传入%rdi,此处printf被优化成了puts函数。
第二处调用过程如图所示:

图3.20 调用过程2

此处三个参数分别被放入%rdx、%rsi、%rdi中。
函数返回:无

3.3.8.3 exit函数

参数传递:传入立即数1。
函数调用:如图所示:

图3.21 调用过程3

此处1放入%edi传入。
函数返回:源程序不涉及返回值。

3.3.8.4 atoi函数

参数传递:是字符串argv[3],在3.3.5分析过存在(%rbp)-8中。
函数调用:如图所示:

图3.22 调用过程4

(%rbp)-8中元素置于%rax,再置于%rdi中传入。
函数返回:返回值为argv[3]中字符串转成的数字,存入%eax中。

3.3.8.5 sleep函数

参数传递:是atoi的返回值,分析得存在%eax中。
函数调用:如图所示:

图3.23 调用过程5

%eax的元素存入%edi,传入函数。
函数返回:源程序不涉及返回值。

3.3.8.6 getchar函数

参数传递:没有参数。
函数调用:如图所示:

图3.24 调用过程6

函数返回:源程序不涉及返回值。

3.4 本章小结

本章介绍了程序编译的概念和作用,和Ubuntu系统中编译所用的代码,并在最后详细分析了hello.s的内容,针对其中数据、赋值、类型转换等等数据类型和操作依次做了详细的说明。我们可以发现,在编译的过程中,编译器会针对我们的语言做一些优化,并非是单纯的一对一关系。我们的hello文件从.i格式变成了.s格式,在成为进程的道路上迈出了巨大的一步。

第4章 汇编

4.1 汇编的概念与作用

汇编的概念:
汇编是将汇编语言编写的源代码,通过运行汇编器,翻译成机器语言的过程。此时生成的文件称为可重定位目标文件。
汇编的作用:
将汇编语言翻译为机器语言,并将相关指令以可重定位目标程序格式保存在.o文件中,此文件包含程序可直接识别的二进制代码。

4.2 在Ubuntu下汇编的命令

汇编的命令:gcc -c -m64 -no-pie -fno-PIC hello.s -o hello.o

图4.1 在Ubuntu下汇编的命令和产生的文件

4.3 可重定位目标elf格式

下图为elf格式的典型格式:

图4.2 elf文件的典型格式

4.3.1 命令

用命令readelf -a hello.o > hello-elf即可将hello.o输出定向到文本文件hello-elf中,如图所示:

图4.3 输出定向命令和产生的文件

4.3.2 ELF头

文件中开始部分是ELF头部分的内容,如图所示:

图4.4 ELF头

ELF头开始处有名为magic的16字节的序列,这个序列的内容讲述了生成该文件的系统的字的大小和字节顺序。余下的部分包含帮助链接器语法分析和解释目标文件的信息,包括ELF头的大小、目标文件的类型、机器类型等等内容。这些内容被翻译成具体内容以中文形式写在下面。

4.3.2 节头部表

在ELF头之后的内容是节头部表,如图所示:

图4.5 节头部表

从图中可以看出,节头表描述了节的基本信息,包括节名称,节的类型,节的属性(读写权限),节在ELF文件中所占的长度以及节的对齐方式和偏移量。这些节的本身意义如下:
.text:已编译程序的机器代码;
.rela.text:.text节中的列表,连接器将此目标文件和其他文件组合式会修改这些位置。
.data:已初始化的静态和全局C变量。
.bss:未初始化的全局和静态C变量,以及所有被初始化0的全局或静态变量。
.rodata:存放只读数据,如prntf中的字符串等。
.comment:包含了版本的控制信息。
.note.GNU_stack:用于标记可执行对栈。
.eh_frame:用于存放异常的处理信息。
.rela.eh_frame:.eh_frame的重定位信息。
.shstrtab:包含节区的名称。
.symtab:存放在程序中定义和引用的函数和全局变量的信息的符号表。
.strlab:存放符号表和节名字的字符串表。
后面的Key to Flags是对这些旗标的解释。

4.3.3 重定位节

节头部表之后是重定位节,其内容如图所示:

图4.6 重定位节

可以看到,重定位节共有两部分,分别是在节头部表里说过的.rela.text和.rela.eh_frame。这两个部分是告诉链接器在将目标文件合并成可执行文件时如何对这些位置的地址进行修改的。当合成可执行文件时,只需调用重定位算法即可计算出应得的地址。
其中.rela.text中含有8条重定位信息,分别对应.L0(第一条字符串)、puts、exit、.L1(第二条字符串)、printf、atoi、sleep、getchar。

4.3.4 符号表

重定位节之后的就是符号表了,其内容如下图所示:

图4.7 符号表

可以看到,符号表就是之前说到的.symtab部分,存放着程序中定义和引用的函数和全局变量的信息。这里一共提供了18个符号,包含main符号和puts、exit等函数等符号。

4.4 Hello.o的结果解析

用命令objdump -d -r hello.o > hello-dis即可把hello.o的反汇编内容输出到hello-dis文本文件中,如图所示:

图4.8 反汇编及产生的反汇编文件

机器语言前部没有.test等指令,中间没有.L2等代码段标识,可以说是只剩下程序操作内容了。
与hello.s对比得知,机器语言反汇编得到的文本文件几乎和汇编语言相同,几乎是一一映射的关系,只有一些小的表示方法不同,如图所示:

图4.9 两个文件的执行方法几乎相同

不同点有以下几点:
1、分支转移
在hello.s中,跳转指令的目标地址直接记为段名称,如.L2、.L3、.L4。而在反汇编得到的文本文件hello-dis中,跳转到的目标变成了地址的偏移量,或者是为其在重定位节.rela.text节中添加的重定位条目。如图所示:

图4.10 hello.s的jmp .L3变成了jmp 80 < main+0x80 >

2、函数调用
在hello.s文件中,call之后直接跟着函数名称,而在反汇编得到的hello-dis中,call 的目标地址是当前指令的下一条指令,同时调用在重定位节rela.text中添加的重定位条目,的如图所示:

图4.11 hello.s的call exit变成了call 2f < main+0x2f >

这是因为hello调用了不确定地址的共享库的函数,这些函数需要在重定位条目中添加,经过计算才能获得其实际的执行地址。
3、字符串的访问
在hello.s文件中,访问字符串使用的是段名称,而在反汇编得到的hello-dis中,访问全局变量用的是0x0,同时调用在重定位节rela.text添加的rodata内容,如图所示:

图4.12 hello.s的mov $.LC1变成了mov 0x0

其原因与函数调用不同的原因大体相同,也是因为调用了不确定地址的字符串内容。
4、数的表示
在hello.s文件中的数是十进制,而在hello-dis文件中数字变成了十六进制,如图所示:

图4.13 hello-dis的数字前面有0x,hello.s没有

4.5 本章小结

本章介绍了程序汇编的概念与作用,学习并输入了命令获得了汇编后的机器语言程序hello.o,以及它的elf格式可重定位目标文件hello-elf,还有其反汇编得到的hello-dis。通过hello-elf文件我们了解了hello.o的完整结构,并分成了ELF头、节头部表等块做了分析;通过hello-dis文件我们发现了hello.s和hello.o逆汇编文件的映射关系和不同之处,并进行了逐一的分析。经过这一章,我们对于汇编的作用和过程有了更深的理解。我们的hello文件也从.s变成了.o。

第5章 链接

5.1 链接的概念与作用

链接的概念:
链接是指通过链接器将各种代码和数据片段收集并组合成为一个单一完全连接的可执行文件(即windows系统下的exe文件)的过程,这个文件可被加载到内存并执行。
链接的作用:
链接既可以执行与编译时,也可以执行于程序加载时,甚至可以执行于程序运行时。它的存在使得分离编译可以实现,使得我们可以更好地管理并修改分块的代码。

5.2 在Ubuntu下链接的命令

链接的命令:ld -o hello -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o hello.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o

图5.1 在Ubuntu下链接的命令及获得的文件

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

用命令readelf -a hello > hello-elf2即可使hello文件输出elf格式文件到文本文件到hello-elf2文件,如图所示:

图5.2 hello文件输出的命令及获得的文件

按照之前的了解,各段的基本信息等都应该保存在节头部表中。此文件的节头部表如下图所示:

图5.3 节头部表2第一部分

图5.4 节头部表2第二部分

节头部表中列举了名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐的相关信息,包含了各段的基本信息内容。

5.4 hello的虚拟地址空间

在edb中打开hello可执行文件,可以在symbols部分看到hello的虚拟地址空间各段信息,如图所示:

图5.5 symbols第一部分

图5.6 symbols第二部分

从图中我们可以清晰地获取到hello文件在虚拟地址上的每一部分内容,将其与之前的节头部表比较后,即可发现它们是一一对应的,例如.interp的位置,在两处都显示是在0x4002e0处,如图所示:

图5.7 节头部表显示的.interp位置

图5.8 symbols显示的.interp位置

5.5 链接的重定位过程分析

使用命令objdump -d -r hello > hello-dis2即可获得hello的反汇编文件hello-dis2。

图5.9 反汇编hello程序命令及生成的产物文件

可以看出,生成的hello-dis2与原先的hello-dis有许多不同点,大致如下:
1、hello-dis2相对原先只有.text节的hello-dis增加了更多的节,如.init节、.plt节等,如图所示:

图5.10 增加了更多的节

2、hello-dis2相对原先的hello-dis引用了更多的库函数,如puts、printf等,如图所示:

图5.11 引用了更多的库函数

3、hello-dis2中使用的地址以及跳转调用中所用的地址都是虚拟内存地址,相对的原先的hello-dis使用的地址都是相对偏移地址,如图所示:

图5.12 左:hello-dis2使用了虚拟内存地址,右:hello-dis使用了相对偏移地址

链接的过程:
连接的过程可分为重定位节、符号定义、在重定位节中引用符号。
重定位节的过程中连接器会合并相同类型的节;
符号定义的过程中连接器会确定所有定义符号在虚拟空间的地址;
在重定位节中引用符号的过程中连接器会用在.rel_data和.rel_text节中保存的重定位信息对引用符号进行重定位,去修改.text节和.data节中对每个符号的引用,进而找到符号应存在的地址。

5.6 hello的执行流程

用终端打开edb,再用edb打开hello文件,在上面使用analyze here功能即可发现动态函数库中依次出现了这些函数:

图5.13出现的函数1

图5.14出现的函数2

这就是在hello外部调用的函数和操作。
进入hello进程后,再次使用analyze here功能,就得到了在hello模块内部依次出现的函数:

图5.15 hello出现的函数

5.7 Hello的动态链接分析

动态链接器是使用过程链接表PLT和全局偏移量表GOT实现函数的动态链接的,其中GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。我们要分析动态链接过程只需要分析PLT和GOT的变化即可。
.got和.got.plt的位置在hello-elf2的节头部表中,如图所示:

图5.16 .got和.got.plt的位置

.got位于403ff0到403fff的位置,.got.plt在404000到404048的位置,在edb中体现为如图所示:

图5.17 .got和.got.plt在edb中

运行hello程序,再查看PLT和GOT,得到如下结果:

图5.18 运行后的.got和.got.plt在edb中

可以看到PLT和GOT有所变化。

5.8 本章小结

本章我们介绍了链接的概念和作用,并且获得了链接后的hello程序,并用命令让其以elf格式输出到hello-elf2中,将其与hello-elf比较;对其进行了反汇编获得了hello-dis2文件,将其与hello-dis做了比较,并从这些内容中分析出了链接的过程,用edb大体了解了hello文件的执行流程,等等。写完这章,我对于链接的过程的理解更加深入;我也对edb的功能之强大叹为观止,并对edb产生了更浓厚的兴趣。我们的hello也从.o文件变成了可执行文件,离成为进程又进了一步。

第6章 hello进程管理

6.1 进程的概念与作用

进程的概念:
进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。程序是指令、数据及其组织形式的描述,进程是程序的实体。
进程的作用:
进程使得计算机系统中显现以下现象:程序好像是系统中当前运行的唯一程序,处理器如同好像是无间断地执行指令,程序中的代码和数据如同是系统内存中唯一的对象;同时进程为程序提供两个关键抽象:独立的逻辑控制流和私有的地址空间。
每次用户向shell输入可执行目标文件的名字,在运行程序时,shell会创建新进程,去运行可执行目标文件。应用程序本身也可创建新进程并运行程序。

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

壳Shell-bash的作用:
Shell是命令语言解释器,是用户与操作系统之间完成交互式操作的一个接口程序,它为用户提供简化了的操作,以此达到解释命令、连接用户和操作系统以及内核的功能。而NU开发的Borne Again Shell,简称bash,是Linux系统中默认的shell程序,合称shell-bash。
壳Shell-bash的处理流程:

  1. Shell首先从命令行中找出特殊字符(元字符),在将元字符翻译成间隔符 号。元字符将命令行划分成小块tokens。Shell中的元字符如下所示:
    SPACE , TAB , NEWLINE , & , ; , ( , ) ,< , > , |
    2.程序块tokens被处理,检查看他们是否是shell中所引用到的关键字。
    3.当程序块tokens被确定以后,shell根据aliases文件中的列表来检查命令 的第一个单词。如果这个单词出现在aliases表中,执行替换操作并且处理过程 回到第一步重新分割程序块tokens。
    4.Shell对~符号进行替换。
    5.Shell对所有前面带有$符号的变量进行替换。
    6.Shell将命令行中的内嵌命令表达式替换成命令;他们一般都采用 $ (command) 标记法。
    7.Shell计算采用 $ (expression)标记的算术表达式。
    8.Shell将命令字符串重新划分为新的块tokens。这次划分的依据是栏位分割 符号,称为IFS。缺省的IFS变量包含有:SPACE , TAB 和换行符号。
    9.Shell执行通配符* ? [ ]的替换。
    10.shell把所有从处理的结果中用到的注释删除,並且按照下面的顺序实行命令的检查:
    A.内建的命令
    B. shell函数(由用户自己定义的)
    C.可执行的脚本文件(需要寻找文件和PATH路径)
    11.在执行前的最后一步是初始化所有的输入输出重定向。
    12.最后,执行命令。

6.3 Hello的fork进程创建过程

用户输入命令./hello程序,shell判断其不是内置命令,而是一个可执行程序,于是调用fork函数来创建子进程。内核为新进程创建各种数据结构,并分配给它一个唯一的PID。它创建当前进程的mm_struct、区域结构和页表的原样副本。fork函数只会被调用一次,但会返回两次,在父进程中,fork返回子进程的PID,在子进程中,fork返回0。创建的子进程与父进程的最大区别在于它们有不同的PID。

图6.1 fork的过程

6.4 Hello的execve过程

shell调用execve函数在当前进程上下文加载并运行hello,函数原型为:int exeve(const char *filename, const char *argv[], const char *envp[]),成功则不返回,发生错误则返回-1。在execve加载了hello之后,它调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数,该主函数有如下形式的原型int main(intargc , char **argv , char *envp);
当main开始执行时,用户栈的典型组织结构如下图所示:

图6.2 用户栈的典型组织结构

6.5 Hello的进程执行

要了解Hello的进程执行,首先我们需要了解以下几个概念:

6.5.1 逻辑控制流

逻辑控制流是进程给程序提供的两个关键抽象之一,它使得程序好像在独占处理器,然而实际操作并非如此。进程中包含了一系列程序计数器的值,这个值的序列就是逻辑控制流,它们的进行是交错的,示例图如下:

图6.3 逻辑控制流示例

如图,A运行一会切到B,再运行一会后切到C。

6.5.2 进程时间片与并发流

一个进程执行他的控制流的一部分的每一个时间段叫做时间片,多任务也叫时间分片。一个逻辑流在执行时间上与另一个流重叠,则两个流称为并发流。

6.5.3 内核模式和用户模式

为了使操作系统内核提供一个无懈可击的进程抽象,处理器必须提供一种机制,限制一个应用可以执行的指令以及它可以访问的地址空间范围。处理器通常是用某个控制寄存器中的一个模式位来提供这种功能的,该寄存器描述了进程当前享有的特权。当设置了模式位时,进程就运行在内核模式中。一个运行在内核模式的进程可以执行指令集中的任何指令,并且可以访问系统中的任何内存位置。没有设置模式位时,进程就运行在用户模式中。用户模式中的进程不允许执行特权指令,比如停止处理器、改变模式位,或者发起一个I/O操作。也不允许用户模式中的进程直接引用地址空间中内核区内的代码和数据。任何这样的尝试都会导致致命的保护故障。
6.5.4 上下文切换
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策就叫做调度。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程,上下文切换做如下操作:
1)保存当前进程的上下文
2)恢复某个先前被抢占的进程被保存的上下文
3)将控制传递给这个新恢复的进程。
如图所示:

图6.4 上下文切换过程

6.5.5 hello的执行过程

在shell执行hello程序时,会先进入用户模式。在用户模式运行时,内核会不断进行上下文切换,并配合调度器将其与其他程序进程交替运行,而在这个过程中如果收到输入的信号,hello进程就会进入内核模式调用信号处理程序,待信号处理完后返回用户模式。

6.6 hello的异常与信号处理

6.6.1 hello的异常

异常可以分为四类:中断、陷阱、故障和终止,所有类别的属性如下:

图6.5 异常的类别

几种异常的处理方式如下图所示:
中断:

图6.6 中断处理

施加O/I输入时,如按键盘,就会触发中断,进入内核模式,调用处理程序并返回。
陷阱:

图6.7 陷阱处理

用户模式无法进行的内核程序,就会触发陷阱,进入内核模式,进行系统调用并返回。
故障:

图6.8 故障处理

进程出现故障时,就会调用故障处理程序。故障可能被修复并返回,但是如果无法被修复的话,就会调用abort终止进程。

图6.9 终止处理

程序出现了致命错误,此时就只能调用中止处理程序,用abort终止进程。

6.6.2 hello信号

Linux的系统信号及信息如下所示:

图6.10 信号列表

6.6.3 hello命令和运行结果截屏

在终端中输入运行的命令,并尝试乱按键盘,如图:

图6.11 乱按键盘

可以看到,随便输入的符号和回车会在程序执行完毕后执行对应内的内容,并会试图把输入的英文解析为命令。
执行时输入Ctrl-Z,会给进程传入SIGTSTP信号,使进程停止,如图所示:

图6.12 Ctrl-Z

此时输入fg %1会使停止的程序重新开始,如图所示:

图6.13 fg %1

输入Ctrl-Z以后,可以像文件中输入的指令以及结果截图如下:
jobs和ps:两者分别列出了jobs的内容和进程中有关hello的项目,如图所示:

图6.14 jobs和ps的内容

pstree:它列出了当前的进程树,如图所示:

图6.15 进程树,圈里是hello及相关进程

kill:直接杀死进程,如图所示:

图6.16 kill命令,kill之后无法调出进程

执行时输入Ctrl-C,会给进程传入SIGINT信号,使进程终止,如图所示:

图6.17 Ctrl-C

6.7本章小结

本章我们分析了进程的概念与作用、shell-bash的作用与处理流程等,并使得hello函数成为了进程,并了解了hello的进程创建过程、进程的执行过程以及执行过程中的信号处理等内容。我们把hello真正从程序变成进程,实现了hello的p2p过程,让我们的hello程序终于破茧成蝶,来到了它生涯的高光时刻,并且使用了bash的一些功能,对其做了截图。写完这章,我对于linux的进程也有了更深入的了解。

第7章 hello的存储管理

7.1 hello的存储器地址空间

hello涉及的各地址定义如下:
逻辑地址:指的是由hello程序产生的段内偏移,在hello.o中用于指定一个操作数或一条指令地址。
线性地址:指的是虚拟地址到物理地址变换的中间层,是处理器可寻址的内存空间中的地址。hello程序代码会产生逻辑地址,也就是段中的偏移地址,加上相应的段基址就成了线性地址。
虚拟地址(VA):指的是CPU启动保护模式后,hello程序在虚拟地址空间运行,会产生由段选择符和段内偏移地址组成的地址,这是访问存储器所用的逻辑地址。
物理地址(PA):指的是CPU外部地址总线上寻址物理内存的地址信号,以电子形式存在,是地址变换的最终结果。

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

一个逻辑地址由两部分组成:段标识符和段内偏移量。段标识符是一个16位长的字段组成,称为段选择符,其中前13位是一个索引号,后面3位包含一些硬件细节,它用来从段描述符表中选择一个具体的段,某个段描述符表项的base字段描述了一个段的开始位置的线性地址。若TI=0,索引全局段描述符表,TI=1,索引局部段描述符表,表的地址在相应的寄存器中,再根据相应的寄存器得到其地址和大小,确保该段可访问,之后即可通过Index字段和段描述符表的位置找到某项具体的段描述符,找到此描述符的base字段,将其与偏移符相加即可获得纤细那个地址。

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

一个N元素的虚拟地址空间(VAS)中的元素和一个M元素的物理地址空间(PAS)中元素之间的映射是地址翻译的过程。其映射关系为:

图7.1 映射关系

其中

图7.2 MAP(A)的限定

展示映射的示例如下图所示:

图7.3 展示映射的示例——页式管理

CPU中的一个控制寄存器,页表基址寄存器指向当前页表。n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移和一个(n-p)位的虚拟页号。MMU利用VPN来选择适当的PTE。例如,VPN 0选择PTE 0,VPN 1选择PTE 1,以此类推。将页表条目中物理页号和虚拟地址中的VPO串联起来,就得到相应的物理地址。

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

7.4.1 TLB

TLB是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块。TLB通常有高度的相联度。虚拟地址用于访问TLB的组成部分如下:

图7.4 虚拟地址用于访问TLB的组成部分

当TLB命中时:
第一步:CPU产生一个虚拟地址。
第二步和第三步:MMU从TLB中取出相应的PLE。
第四步:MMU将这个虚拟地址翻译成一个物理地址,并且将它发送到高速缓存/主存。
第五步:高速缓存/主存将所请求的数据字返回给CPU。
由于所有的地址翻译步骤都在芯片上的MMU中执行的,所以非常快。
当TLB不命中时,MMU必须从L1缓存中取出相应的PTE,新取出的PTE存放在TLB中,可能会覆盖一个已经存在的条目。用TLB的情况下VA到PA变化如下:

图7.5 TLB命中和不命中的情况

7.4.2 四级页表

用来压缩页表的常用方法是使用层级结构的页表。一级页表中的每个PTE负责映射虚拟地址空间中的一个片,每一片都由多个连续的页面组成。如果片i中的每个页面都未被分配,那么一级PTE i就为空。以同样的方法,二级页表映射三级页表,三级页表映射四级页表。
四级页表中的每个PTE负责映射一个虚拟内存页面,就想我们查看只有一级的页表一样。
对于使用四级页表层次结构的地址翻译,虚拟地址被划分成为四个VPN和一个VPO。每个VPN i都是一个到第i级页表的索引,其中1≤i≤4。第j级页表中的每个PTE,1≤j≤3,都指向第j+1级的某个页表的基础。第四级页表中的每个PTE包含某个物理页面的PPN,或者一个磁盘块的地址。在能够确定PPS之前,MMU必须访问4个PTE。图为使用k级列表时的地址翻译:

图7.6 使用k级列表时的地址翻译

在本程序中,k=4,地址翻译流程如下图:

图7.7 使用4级列表时的地址翻译

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

CPU发送一条虚拟地址,随后MMU按照上述操作获得了物理地址PA。我们将地址PA由高至低划分为四个部分:标签、索引、块内偏移、字节偏移。
用索引定位到相应的缓存块。用标签尝试匹配该缓存块的对应标签值。如果存在这样的匹配,称为命中;否则称为未命中。如果命中就直接返回想要的数据,如果不命中,就依次去L2,L3,主存判断是否命中,当命中时,将数据传给CPU同时更新各级cache的cacheline(如果cache已满则要采用换入换出策略)。

图7.8 三级cache支持下的物理内存访问

7.6 hello进程fork时的内存映射

当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID,同时为这个新进程创建虚拟内存。它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记位只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面。因此,也就为每个进程保持了私有空间地址的抽象概念。

7.7 hello进程execve时的内存映射

execve函数在当前进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效的替代了当前程序。加载并运行hello需要以下几个步骤:
1.删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
2.映射私有区域。为新程序的代码、数据、bss和栈区域创建新的区域结构。所有的这些新的区域都是私有的、写时复制的。私有区域的不同映射图片如下:

图7.9 私有区域的不同映射

3.映射共享区域。hello程序与共享对象的链接都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
4.设置程序计数器。execve做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域段的入口点。
下次调度这个进程时,它将从这个入口点开始执行。

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

页面命中完全是由硬件完成的,而处理缺页是由硬件和操作系统内核协作完成的,流程如下图所示:

图7.10 缺页故障与缺页中断的处理

发生缺页故障与缺页中断时,缺页中断处理如下:
第一步:处理器生成一个虚拟地址,并将它传送给MMU。
第二步:MMU生成PTE地址,并从高速缓存/主存请求得到它。
第三步:高速缓存/主存向MMU返回PTE。
第四步:PTE中的有效位是0,所以MMU出发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序。
第五步:缺页处理程序确认出物理内存中的牺牲页,如果这个页已经被修改了,则把它换到磁盘。
第六步:缺页处理程序页面调入新的页面,并更新内存中的PTE。
第七步:缺页处理程序返回到原来的进程,再次执行导致缺页的命令。CPU将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面已经换存在物理内存中,所以就会命中。

7.9动态存储分配管理

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

图7.11 堆的示意图

系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的区域后开始,并向上生长。对于每个进程,内核维护着变量brk,它指向堆的顶部。
分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显示地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显示地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序执行的,要么是内存分配器自身隐式执行的。
分配器有两种基本风格。两种风格都要求应用显示地分配块。它们的不同之处在于由哪个实体来负责释放已分配的块。
1.显式分配器,要求应用显式地释放任何已分配的块。
2.隐式分配器,要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器,而自动释放未使用的已分配块的过程叫做垃圾收集。
显式分配器的策略包括如下:
1、显式分配器必须在严格的约束条件下工作
必须处理任意请求序列;立即响应请求;只使用堆;对齐块;不修改已分配的块。
2、分配器的编写应该实现:吞吐率最大化;内存使用率最大化(两者相互冲突)。
隐式分配器的策略包括放置策略,放置策略中常见的策略是首次适配(first fit)、下一次适配(next fit)和最佳适配(best fit)。
首次适配从头开始搜索空闲链表,选择第一个合适的空闲块。下一次适配和首次适配很相似,只不过不是从链表的起始处开始每次搜索,而是从上一次查询结束的地方开始。最佳适配检查每个空闲块,选择适合所需请求大小的最小空闲块。

7.10本章小结

本节我们主要以书本知识为基础,分析了hello占用的存储的过程中地址的变换流程,以及电脑硬件在这个流程中发挥的作用,还有hello在执行时的内存映射以及缺页中断等等。通过这一章,我对虚拟内存部分的知识有了更深的理解,我也知道了hello是以何种方式存在于程序当中的。hello调用TLB等获取物理地址的过程和方法,正是计算机系统学习的关键。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

设备的模型化:
文件:所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。
设备管理:
Unix I/O接口:将设备映射为文件的方式允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行。我们可以对文件的操作有:打开关闭操作open和close;读写操作read和write;改变当前文件位置lseek等

8.2 简述Unix IO接口及其函数

Unix IO接口的内容:
1、打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个 I/O 设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
2、Linux shell 创建的每个进程开始时都有三个打开的文件:标准输入(描述符为 0)、标准输出(描述符为1)和标准错误(描述符为2)。头文件 <unistd.h> 定义了常量 STDIN_FILENO、STDOUT_FILENO 和 STDERR_FILENO,它们可用来代替显式的描述符值。
3、改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k,初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek操作,显式地设置文件的当前位置为k。
4、读写文件。一个读操作就是从文件复制n > 0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。给定一个大小为m字节的文件,当时执行读操作会触发一个称为end-of-file(EOF)的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF符号”。
5、关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
Unix IO接口的函数:
open函数:int open(char *filename, int flags, mode_t mode);
open函数将filename转换为一个文件描述符,并返回描述符数字,若成功则为新文件描述符,若出错为-1。返回的描述符总是在进程中当前没有打开的最小描述符。
close函数:int close(int fd);
通过调用close函数关闭一个打开的文件,fd 是需要关闭的文件的描述符。若成功返回0,否则为-1。
read函数:ssize_t read(int fd, void *buf, size_t n);
read函数从描述符fd的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,返回值0表示EOF。否则返回值表示的是实际传送的字节数量。
write函数:ssize_t write(int fd, const void *buf, size_t n);
write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。若成功则返回写的字节数,若出错则返回-1。

8.3 printf的实现分析

printf的代码实现如下:

int printf(const char *fmt, ...){int i;char buf[256];va_list arg = (va_list)((char*)(&fmt) + 4);i = vsprintf(buf, fmt, arg);write(buf, i);return i;
}

可以看出,printf函数是按照格式fmt生成字符串的,它将fmt和后续参数arg、buf部分一并传入了vsprintf函数,然后调用了write函数,并获取了它的返回值,即输出长度作为返回值。
vsprintf函数代码实现如下:

int vsprintf(char *buf, const char *fmt, va_list args) { char* p; char tmp[256]; va_list p_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的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出,同时返回要打印出来的字符串的长度。
另一个涉及到的函数,write,是一个系统函数,反汇编得到它的代码如下:

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

这个函数的作用是把buf中i个元素的值输出到终端。
函数中调用了INT_VECTOR_SYS_CALL,它表示要通过系统来调用sys_call这个函数。
sys_call的内容:

call save
push dword [p_proc_ready]
sti
push ecx
push ebx
call [sys_call_table + eax * 4]
add esp, 4 * 3
mov [esi + EAXREG - P_STACKBASE], eax
cli
ret

函数的作用:显示格式化的字符串。将要输出的字符串从总线复制到显卡的显存中。
复制到显存之后,后面的过程如下:
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

getchar()函数的具体实现如下:

int getchar(void)
{static char buf[BUFSIZ];static char* bb=buf;static int n=0;if(n==0){n=read(0,buf,BUFSIZ); bb=buf;}return(--n>=0)?(unsigned char)*bb++:EOF;
}

其作用在于标准的输入stdin中读取字符。
首先,函数定义了指向缓冲区第一个位置的指针,然后调用了read系统函数,在该指针的位置读入字符。程序等待用户输入。用户输入字符,触发中断,触发事件是键盘按下,通过系统调用获取案件ascii码,将对应的字符存入缓冲区,按下回车时,系统调用程序返回,并使getchar()返回读入的第一个字符。

8.5本章小结

在这一章中,我们了解了linux系统下的IO接口管理方法及其调用的函数,并利用这些函数对printf和getchar两个函数做了接口层面的分析。我对于程序中函数的调用与实现有了更深的见解。

结论

开始时,hello文件还是一个高级语言文件,以hello.c的形式存在着,而它的程序周期也是从.c格式开始的;
1、hello.c经过预处理,变成了保留预处理内容的文本文件hello,i;
2、hello.i经过编译程序,变成了保存汇编代码的汇编文件hello.s;
3、hello.s经过汇编程序,生成了保存二进制机器代码的可重定位目标文件hello.o;
4、hello.o通过与系统库进行链接,生成了可执行文件hello;
5、shell-bash进程调用fork函数,生成hello进程,又由execve函数加载运行当前进程上下文并运行程序hello;
6、hello在进程中通过多级页表、TLB等机制实现从虚拟内存(VA)到物理内存(PA)的转变,并运用动态内存,以达到和计算机交互的结果;
7、hello在运行时调用的函数中包含Unix I/O提供的函数,它用这些函数实现和I/O设备的交互。
8、hello运行结束,shell对其进行回收,内核收回为其创建的所有信息。
经过一个学期计算机系统的学习,我认识到了计算机系统是一门复杂而实用的学科。一方面,计算机系统方方面面内容繁多,且分为不同层面,硬件层面、软件层面等,每个层面都有许多内容,要学会这些内容很困难;另一方面,这些内容都是每个程序直接经历的内容,每个层面、每个操作,程序基本都有涉及,即使是一个简简单单的hello.c文件,就要经历这所有的步骤,从一步步变成可执行文件,到被创立进程执行,再到被回收,等等。这正说明了计算机系统对于程序员的重要性。学好计算机系统,对我这样的计算机专业学生是至关重要的,而我在这方面上也任重而道远。

附件

文件名 函数作用
hello.i hello.c经过预处理后的文件
hello.s hello.i经过编译后的汇编文件
hello.o hello.s经过汇编后的可重定位目标文件
hello hello.o经过链接之后的可执行目标文件
hello-elf hello.o的ELF格式文件
hello-dis hello.o 经过反汇编获得的反汇编文件
hello-elf2 hello的ELF格式文件
hello-dis2 hello 经过反汇编获得的反汇编文件

参考文献

[ 1 ]Randal E. Bryant, David R. O’Hallaon. 深入理解计算机系统. 第三版. 北京:机械工业出版社[M]. 2018: 1-737
[ 2 ] 博客园.printf 函数实现的深入剖析.https://www.cnblogs.com/pianist/p/3315801.html
[ 3 ]ELF 格式详解(一). https://zhuanlan.zhihu.com/p/73114831

哈尔滨工业大学计算机系统大作业-hello的程序人生相关推荐

  1. 哈尔滨工业大学计算机系统大作业--程序人生

    计算机系统   大作业 题     目  程序人生-Hello's P2P      专       业   计算机科学与技术        学    号        2021110xxx      ...

  2. 哈尔滨工业大学计算机系统大作业 程序人生-Hello’s P2P

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算学部 计算机科学与技术学院 2021年5月 摘  要 本文以hello程序从hello.c到进程各个阶段所 ...

  3. 哈尔滨工业大学 计算机系统大作业

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算机类 学   号 120L022010 班   级 2003007 学       生 窦馨宇 指 导 教 ...

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

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算机 学   号 120L02**** 班   级 2003005 学       生 无敌飞龙 指 导 教 ...

  5. 2023春 哈尔滨工业大学计算机系统大作业

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 物联网工程 学    号 2021111491 班    级 2137301 学       生 业知翰 指 ...

  6. 2022春哈工大计算机系统大作业——hello的程序人生

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算学部 学   号 班   级 学       生 指 导 教 师 计算机科学与技术学院 2021年5月 摘 ...

  7. 哈尔滨工业大学 计算机系统 大作业 22春

    计算机系统大作业 计算机科学与技术学院 2022年5月 摘 要 摘要是论文内容的高度概括,应具有独立性和自含性,即不阅读论文的全文,就能获得必要的信息.摘要应包括本论文的目的.主要内容.方法.成果及其 ...

  8. 哈尔滨工业大学计算机系统大作业论文-程序人生-Hello‘s P2P

    文章目录 第1章 概述 1.1 Hello简介 1.2 环境与工具 1.3 中间结果 1.4 本章小结 第2章 预处理 2.1 预处理的概念与作用 2.2在Ubuntu下预处理的命令 2.3 Hell ...

  9. 哈尔滨工业大学计算机系统大作业——程序人生-Hello’s P2P

    目录 摘要 第1章 概述 1.1 Hello简介 1.1.1 P2P:From Program to Process 1.1.2 020:From Zero-0 to Zero-0 1.2 环境与工具 ...

最新文章

  1. Python3 turtle海龟绘图入门代码示例
  2. Severstal: Steel Defect Detection比赛的discussion调研
  3. python实现快排算法_Python实现快速排序算法
  4. 如何快糙好猛的使用Shiqi.Yu老师的公开人脸检测库(附源码)
  5. 【Android开发】NDK开发(1)-Hello World!
  6. win10家庭版删除文件提示没有权限最简单的方式
  7. UVA1437 String painter
  8. php71+yum源+epel,搭建CentOS在线yum源镜像服务器
  9. postgres报错:FATAL: Peer authentication failed for user “zabbix“ Previous connection kept
  10. 阶段1 语言基础+高级_1-3-Java语言高级_06-File类与IO流_07 缓冲流_5_BufferedWriter_字符缓冲输出流...
  11. 数独基本规则_数独游戏规则?
  12. 外贸推荐:询盘回复的全面流程
  13. 如何搭建ftp服务器实现文件共享
  14. 蚂蚁森林自动收能量之AutoJs实现
  15. catia制作物料明细_CATIA导出装配文件的部件列表BOM清单到Excel文件 | 坐倚北风
  16. 深圳第一职业技术学校计算机分数线,2018年深圳第一职业技术学校第一批中招录取分数线:382分...
  17. 如何将CAD图块转换成外部参照?
  18. 教你用 python 制作一张五彩斑斓的黑
  19. 证书扫描件怎么压缩?如何将电子版合同资料压缩变小?
  20. ArcMap10.6以上版本添加天地图底图

热门文章

  1. 微信小程序中如何将图片铺满屏幕,作为背景图片
  2. Android调用系统摄像头拍照并显示在ImageView上
  3. Linux中常见的150个命令(干货)
  4. smortupload上传下载
  5. IDEA 2021安装教程
  6. 水性防腐涂料行业调研报告 - 市场现状分析与发展前景预测
  7. 生产线平衡常见的几个概念及线平衡改善
  8. 希伯特数值与二维坐标的相互转换(python)
  9. 解决Google Chrome启动页面被篡改锁定和劫持
  10. 使用AspectJ LTW(Load Time Weaving)