计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术
学 号 202111xxxx
班 级 21xxxxx
学 生 xxx
指 导 教 师 刘宏伟
计算机科学与技术学院
2022年11月
摘 要
本文将从最简单的程序——hello谈起,讲述一个程序从我们按下运行键开始直至运行结束,进程被回收时所经历的所有过程。借助《深入理解计算机系统》这本书的研究顺序,结合edb、gcc,shell等工具,研究明白从hello出生到死亡,都经历了什么。
预处理、编译、汇编、链接,这是hello作为Program经历的过程。看似简单的四个名词却能让计算机读懂hello,并能够让hello有机会在计算机上形成自己的进程。从汇编代码的阅读,再到反汇编代码的解读,我们尽全力在了解hello这一生的每一步都做了些什么,究竟计算机在作何尝试让hello绽放其生命的色彩。
从进程管理,到存储管理,hello此刻作为进程,在其短暂的一生中也会在计算机里面留下自己的痕迹,我们要尝试着去查找这个痕迹,找到hello存在过的证明。
跟着hello走完它的一生,我们第一次懂得了程序员的浪漫,也让我们更加清晰地知道了每日与我们朝夕相处的程序的运行流程,我认为这就是hello带给我们的最宝贵的财富。
关键词:P2P;预处理;编译;汇编;链接;进程管理;
目 录
第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简介
1.1.1. P2P
P2P全称是’From Program to Process’,翻译过来就是从程序到进程。以Hello为例,Hello最开始的形态是Hello.c,是我们在编译器中编写出的代码,也就是program。而后经过预处理、编译、汇编、链接四个过程成为可执行文件Hello。这个时候我们运行Hello,shell会进行解析命令行参数,初始化环境变量等一系列操作。然后会调用fork函数创建进程,execve函数运行函数,通过内存映射,分配空间等让Hello与其他进程并发进行,到这里Hello就顺利变成了进程。这就是Hello的 P2P的过程。
1.1.2. 020
020全称是’From Zero to Zero’,,翻译过来就是从0到0。在程序运行之前,在计算机中并没有什么,所以是从0开始,而后在运行的时候拥有了自己的进程,也在内存中存储了相关信息。但最终在进程终止之后,这一切都会被回收并释放,最终什么都没有留下,所以是以0结束。这就是Hello从0到0的一生。
1.2 环境与工具
硬件环境:X86-64 CPU;16GB RAM;512GB SSD
软件环境:Windows10 64位;VMware Workstation Pro16.2.2;Ubuntu 20.04.3
开发和调试工具:gcc;edb;readelf;objdump;Code::Blocks20.03
1.3 中间结果
hello.i:hello.c预处理后的文件。
hello.s:hello.i编译后的文件。
hello.o:hello.s汇编后的文件。
hello:hello.o链接后的文件。
hello1asm.txt:hello.o反汇编后代码。
hello2asm.txt:hello反汇编后代码。
hello.o.elf:hello.o用readelf -a hello.o指令生成的文件。
hello.elf:hello用readelf -a hello指令生成的文件。
1.4 本章小结
本章介绍了什么是P2P和020,并从P2P和020两个方面对于hello进行了简短的介绍。其次说明了一个程序从开始运行到形成进程,再到最终被回收的详细过程,这也是本次大作业的一个基本的脉络。最后对于本次大作业的环境和一些用到的文件进行了表述。
第2章 预处理
2.1
2.1.1预处理的概念:
预处理指的是在程序的源代码被编译之前,通过预处理器对于程序源代码的可编译性进行提升的代码处理过程。值得注意的是,这个过程只进行一些为了之后的编译而分割或者处理特定代码的操作,而并不对于代码进行解析。一般的处理为支持宏调用。
2.1.2预处理的作用
(1)文件包含:对于#include的处理是一种常见的预处理,预处理的方法是复制其引用的文件进入程序的文本之中。
(2)条件编译:对于#if、#ifdef、#undef、#ifndef等的处理是一种常见的预处理,预处理的方法是直接加入语句对于相关部分进行限定。
(3)布局控制:对于#pragma的处理是一种常见的预处理,其主要的作用是为编译程序提供非常规的控制流类型。
(4)宏定义:对于#define的处理是最为常见的预处理,这种预处理实现了定义符号变量,函数功能,重命名,字符串拼接等相关处理,预处理的方法非常简单,就是在程序文本之中进行直接替换即可。
2.2在Ubuntu下预处理的命令
打开终端,输入gcc –E hello.c –o hello.i 或 cpp hello.c > hello.i ,即可生成文本文件hello.i。
正在上传…重新上传取消
图2-2-1 Ubuntu下预处理指令gcc及结果
正在上传…重新上传取消
图2-2-2 Ubuntu下预处理指令cpp及结果
2.3 Hello的预处理结果解析
观察到所有的#include等语句全部被替换,取而代之的是一些路径以及用到的相关语句被插入了该文本。并且在代码中插入注释后进行重新预处理并向下观察可以发现,预处理同时也删除了所有的注释信息。
打开hello.i程序,程序是可读的文本文件,总共有3060行。观察发现,其中的注释已经消失,前一部分的代码为,被加载到程序中的头文件;程序的最后一部分与hello.c中的main函数完全相同。
正在上传…重新上传取消
2.4 本章小结
本章首先介绍了预处理的概念与作用,接着以hello.c为例,演示了在Ubuntu下如何预处理程序,并对结果进行了分析。
第3章 编译
3.1 编译的概念与作用
3.1.1.编译的概念
编译就是利用编译程序从源语言编写的源程序产生目标程序的过程。编译程序的工作就是通过编译器的词法分析和语法分析,先行确认是否存在不符合语法规则的情况,而后进行语义分析,将上述的预处理结果翻译成汇编代码。编译一般包括语法分析、中间代码、代码优化、目标代码这几个基本流程:
3.1.2.编译的作用
将高级语言书写的源程序转换为一条条机器指令,机器指令和汇编指令一一对应,使机器更容易理解,为汇编做准备。
3.2 在Ubuntu下编译的命令
打开终端,输入cc1 hello.i -o hello.s 或 gcc -S hello.c -o hello.s
正在上传…重新上传取消
图3-2-1 Ubuntu下用gcc编译
正在上传…重新上传取消
图3-2-2 Ubuntu下用cc1编译
3.3 Hello的编译结果解析
编译过程是整个过程构建的核心部分,编译成功,源代码会从文本形式转换为机器语言。
下面是hello.s汇编文件内容:
3.3.1对文件信息的记录
正在上传…重新上传取消
图3-3-1 hello.s文件内容
首先是记录文件相关信息的汇编代码,在之后链接过程使用。其中.file指明了源文件,.text为代码段,.section和 .radata为只读代码段,.align表示对齐方式为8字节对齐,.string为字符串,.global为全局变量,.type声明main是函数类型。
3.3.2对局部变量的操作
正在上传…重新上传取消
图3-3-2-1 hello.c文件内容
正在上传…重新上传取消
图3-3-2-2 hello.s文件内容
局部变量存储在栈中,当进入函数main的时候,会根据局部变量的需求,在栈上申请一段空间供局部变量使用。当局部变量的生命周期结束后,会在栈上释放。
在hello.c中i是局部变量,在hello.s中可以看到,首先跳转到了.L3的位置,然后将栈指针减少4,即存储局部变量i,然后跳转到.L4进行接下来的操作。
3.3.3对字符串常量的操作
在main函数前,在.rodata处的.LC0和.LC1已经存储了字符串常量,标记该位置是代码是只读的。在main函数中使用字符串时,得到字符串的首地址,如下图。
正在上传…重新上传取消
图3-3-3 hello.s文件内容
3.3.4对立即数的操作
立即数直接用$加数字表示
正在上传…重新上传取消
图3-3-4 hello.s文件内容
3.3.5 参数传递
在main函数的开始部分,因为后面还会使用到%rbp数组,所以先将%rbp压栈保存起来。21行将栈指针减少32位,然后分别将%rdi和%rsi的值存入栈中。
由此我们知道,%rbp-20和%rbp-32的位置分别存了argv数组和argc的值。
正在上传…重新上传取消
图3-3-5 hello.s文件内容
3.3.6 数组的操作
对数组的操作,都是先找到数组的首地址,然后加上偏移量即可。例如在main中,调用了argv[1]和argv[2],在汇编代码中,每次将%rbp-32的的值即数组首地址传%rax,然后将%rax分别加上偏移量16和8,得到了argv[1]和argv[2],在分别存入对应的寄存器%rsi和%rdx作为第二个参数和第三个参数,之后调用printf函数时使用。
正在上传…重新上传取消
图3-3-6 hello.s文件内容
调用完printf后,同样在偏移量为24时,取得argv[3]并存入%rdi作为第一个参数在调用函数atoi时使用。
3.3.7对函数的调用与返回
函数的前六个参数由寄存器传参,返回值存在%rax寄存器中。在函数调用时,先将相应的值存入相应的寄存器,然后使用call指令调用函数和ret指令返回。注意,由于函数是共用一套寄存器的,在调用一个函数之前,要先将当前函数的一些值保存起来,调用完再恢复。
对printf函数的调用,在3.3.6中已经介绍过,取得argv数组的第二个和第三个元素放入寄存器%rsi和%rdx,然后41行取得了字符串的地址,并存入了%rdi中作为第一个参数,这样三个参数都准备好后,用call指令调用了printf函数。
正在上传…重新上传取消
图3-3-7-1 hello.s文件内容
对atoi函数和sleep函数的调用,先取得argv存入%rdi作为第一个参数,然后第48行call指令调用了atoi函数,接着将atoi的返回值存入%rdi中作为sleep的第一个参数,然后用call调用sleep函数。
正在上传…重新上传取消
图3-3-7-2 hello.s文件内容
3.3.8 for循环
对于for循环,将循环变量存入一个寄存器中,然后当执行完一个循环体之后,更新循环变量,然后用cmp指令将其与条件进行比较,满足则继续,否则退出循环。
正在上传…重新上传取消
图3-3-8 hello.s文件内容
3.3.9 赋值操作
赋值操作很简单,用movq指令即可,例如将a寄存器的值赋值给b寄存器,用movq a b(以8字节为例)。
在hello.s中很多地方都用到了赋值语句,比如说对局部变量i的赋值:
正在上传…重新上传取消
图3-3-9 hello.s文件内容
3.4 本章小结
本章首先介绍了编译的概念和作用,然后在Ubuntu下以hello.s为例,通过分析其汇编程序,理解编译器是如何处理各种数据类型和实现各类操作的。编译是从高级语言程序生成可执行文件的过程中的关键一步。
第4章 汇编
4.1 汇编的概念与作用
4.1.1汇编的概念
把汇编代码翻译成其他语言指令,把这些指令打包成为可重定位目标程序的格式,并形成.o文件的过程就是汇编。.o文件是一个二进制文件,它所包含的字节是函数main的指令编码。这也就导致了如果我们在文本编辑器中尝试打开hello.o文件看到的将是乱码。汇编的过程由汇编器来完成,由于每一条汇编语句都基本对应一条机器指令,所以过程不必分析语法、语义等,只需要按照翻译表一一对照着翻译就可以了。
4.1.2汇编的作用
汇编的过程将汇编代码转换为计算机能够理解并执行的二进制机器代码,这个二进制机器代码是程序在本机器上的机器语言的表示,从而使程序真正可以被执行。
4.2 在Ubuntu下汇编的命令
打开终端输入 as hello.s -o hello.o 或 gcc -c hello.s -o hello.o
正在上传…重新上传取消
图4-2-1 Ubuntu下用as汇编
正在上传…重新上传取消
图4-2-2 Ubuntu下用gcc汇编
4.3 可重定位目标elf格式
hello.o分析:
1、首先,打开终端,用readelf -S指令查看hello.o的节头表,查看节的基本信息。除了ELF头外,节头表的是ELF可重定位目标文件中最重要的部分。描述了每个节的节名文件偏移、大小、访问属性、对齐方式等。
正在上传…重新上传取消
图4-3-1 readelf -S指令查看节头表
2、用readelf -h指令可以查看hello.o的ELF头信息。
类别:64位版本
数据:使用补码表示,且为小端法
Version:版本为1
OS/ABI:操作系统为UNIX – SYSTEM V
类型:REL表明这是一个可重定位文件
系统架构:64位机器上编译的目标代码为Advanced Micro Devices X86-64
入口点地址:为0x0表示程序的入口地址为0
程序头起点:为0表示没有程序头表
Start of section headers:节头表的起始位置为1240字节处
Size of this header:64表示每个表项64个字节
Number of section headers:14表示共14个表
Section header string table index:13为.strtab在节头表中的索引
正在上传…重新上传取消
图4-3-2 readelf -h指令查看ELF头
3、readelf -s hello.o查看符号表,Name为符号名称,Value是符号相对于目标节的起始位置偏移,Size为目标大小,Type是类型,数据或函数,Bind表示是本地还是全局。
正在上传…重新上传取消
图4-3-3 readelf -s指令查看符号表
4、readelf -r hello.o发现hello.o文件中没有.rel.data段,即.data节不需要额外的重定位信息。
对于.rel.text节,偏移量为需要重定位的地址与该段首地址的偏移;信息高24位为所引用的符号索引,低8位对应的重定位类型;类型有两种类型,一种是R_X86_64_PC32,表示使用的是32位PC相对地址的引用,另一种是R_X86_64_32,表示使用的是32位绝对地址的引用;符号名称为绑定的符号名, 加数为偏移。
正在上传…重新上传取消
图4-3-4 readelf -r指令查看重定位信息
5、readelf -g hello.o显示节组信息。显示hello.o没有节组
正在上传…重新上传取消
图4-3-5 readelf -S指令查看节组信息
4.4 Hello.o的结果解析
在终端输入objdump -d -r hello.o查看hello.o的反汇编(此外还使用了命令objdump -d -r hello.o > hello1asm.txt生成hello.o返汇编文件hello1asm.txt),结果如下:
正在上传…重新上传取消
图4-4-1 objdump指令查看反汇编
正在上传…重新上传取消
图4-4-2 objdump指令查看反汇编
Hello.s文件内容如下:
正在上传…重新上传取消
图4-4-3 hello.s文件内容
正在上传…重新上传取消
图4-4-4 hello.s文件内容
通过对比,我们能够发现如下的不同点:
4.4.1. 分支转移
在上面分析hello.s中,跳转指令的地址直接记为段名称,例如.L2、.L3等,但是在hello1asm.txt中,跳转的目标是具体的地址,在机器代码中体现为目标指令地址与当前指令下一条指令的地址差。
4.4.2. 函数调用
在上面分析hello.s中,call后面直接就能够读取函数的名称,但是在我们反汇编得到的hello1asm.txt中,call的目标地址却是当前的指令的下一条指令。这是因为hello.c中调用的函数例如getchar、printf、exit等都是共享库的函数,这些都需要动态链接器的作用才能够确认函数运行的时候的执行地址,但介于在汇编成为机器语言之后还没有进行上述操作,所以现在call指令后面对应的相对地址被设置为了0(此时目标地址就是下一条指令)。然后通过添加重定位信息,等待静态链接进一步确定。所以目前从call后面读取到的讯息并不可靠,也不会是我们即将调用函数的有效执行地址。此外,所有函数的地址都是以main函数的地址为基准,然后通过main函数的相对偏移地址进行使用的,因为其余函数只有在链接之后才能确定运行执行的地址。故它们也对应着重定位条目。
4.4.3. 全局变量访问
出现的情况与函数调用类似。在hello.s中直接通过段名称+%rip访问rodata,但是在hello1asm.txt中由于不知道rodata的数据地址,所以只能先写成0+%rip进行访问,而后在后续的操作中利用重定位和链接来实现对于rodata的访问。
4.4.4. 数的表示
hello.s里面的数是十进制表示,hello1asm.txt里面的数是十六进制表示。
4.4.5. 分支转移
hello.s中在跳转时直接使用的是段的名称,hello1asm.txt中使用的是间接地址。
4.5 本章小结
本章首先介绍了汇编的概念和作用,接着通过实操,对hello.s文件进行汇编,生成ELF可重定位目标文件hello.o。接着我们从两个角度来认识了hello.o这个文件,首先是可重定位目标elf格式文件,我们对于elf头、节头表、重定位节和符号表分别进行了分析。然后我们将hello.o的反汇编与hello.c的汇编进行了对比,更深刻地认识了重定位和链接的作用,这也为下一章的工作做好了铺垫。
第5章 链接
5.1 链接的概念与作用
5.1.1链接的概念
链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,也是源代码能够成功运行的最后一项前置工作。组合成的单一文件可以被加载、复制到内存并执行。链接有多种执行的模式,可以在编译时候执行,也可以在加载的时候执行,还可以在运行的时候执行。这三种执行模式分别对应着三个阶段:源代码被翻译成机器代码的过程;程序被加载器加载到内存并执行的过程;应用程序接盘进程的过程。
5.1.2链接的作用
链接使得分离编译成为了可能,不再非要把程序都写在一个源文件中,而是可以分解为许多的小文件,把工程模块化。并且这样操作可以使得每个模块可以独立修改,独立操作,修改后只需要单独进行重新编译即可,而不是像之前一样没修改的部分也要被迫参与重新编译,从而大大节约了资源。
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 -o hello来进行链接如图:
转存失败重新上传取消
图5-2Ubuntu下链接
5.3 可执行目标文件hello的格式
使用命令:readelf -a hello > hello.elf
正在上传…重新上传取消
图5-3-1:hello.elf文件
通过初步观察可以发现从整体的格式到内容,这个hello.elf和上面的hello.o.elf基本相同,就是hello.elf的内容相较于hello.o.elf多了许多。下面进行重点分析:
5.3.1. elf头
hello.elf的elf头和hello.o.elf的elf头所包含的信息种类基本相同,仍然是从Magic开始,到帮助链接器语法分析和解释目标文件的信息。不同的是程序头大小和节头数量都得到了增加,更重要的是获得了入口地址。hello.elf的elf头信息如下:
正在上传…重新上传取消
图5-3-2:elf头
5.3.2. 节头表
相较于hello.o.elf,hello.elf内容更加丰富详细。hello.elf的节头表部分信息如下:
正在上传…重新上传取消
图5-3-3:节头表部分代码
分析可以发现,当完成链接之后,程序中的一些文件就被添加进来了,这也证明我们的链接是有效的。此外,每一节都有了实际地址也可说明这一点。
5.3.3. 重定位节
正在上传…重新上传取消
图5-3-4:重定位节代码
可以发现hello.elf的重定位节与hello.o.elf的重定位节的名字以及内容都完全不一样,现在的所有加数都是0,这能够证明在链接环节确实完成了我们上文提到的各种重定位效果。
5.3.4.符号表
正在上传…重新上传取消
图5-3-5:符号表的部分代码
与hello.o.elf不同的是,main函数以外的符号也拥有了type。相同的是符号表的功能仍然没有发生变化,所有重定位需要引用的符号都在其中说明。
5.3.5. 程序头
程序头是一个结构数组,描述了系统准备程序执行所需的段或者其他信息。程序头部分的信息如下:
正在上传…重新上传取消
图5-3-6:程序头代码
程序头描述了可执行目标文件的连续的片与连续的虚拟内存段之间的映射关系。从程序头中可以看到根据可执行目标文件的内容初始化为了两个内存段,分别为只读内存段(代码段)和读写代码段(数据段)。
5.4 hello的虚拟地址空间
与节头表对应的部分:
正在上传…重新上传取消
图5-4-1:edb的Date Dump信息
可以发现程序的虚拟地址从0x401000开始到0x402000结束,这与.elf文件的节头表部分正好对应。此外查看symbols选项也能发现程序各部分的地址都可以与.elf的节头表完全对应,如图:
正在上传…重新上传取消
图5-4-2:edb的Symbols信息
与elf头对应的部分:
通过查看地址为0x400000的位置对应到了elf头的magic部分,向后阅读也能读到字符串的部分。
5.5 链接的重定位过程分析
首先,使用objdump -d -r hello对hello进行反汇编(同时使用命令objdump -d -r hello > hello2asm.txt将结果保存进hello2asm.txt文件),结果如下:
正在上传…重新上传取消
图5-5-1 objdump查看hello反汇编结果
可以发现hello的反汇编代码多了很多节,并且每条数据和指令都已经确定好了虚拟地址,不再是hello.o中的偏移量。通过链接之后,也含有了库函数的代码标识信息。
接着,我们具体比较分析一下hello和hello.o的反汇编结果,下面两个图分别为hello.o和hello的反汇编的部分截图。
正在上传…重新上传取消
图5-5-2 hello.o反汇编部分截图
正在上传…重新上传取消
图5-5-3 hello反汇编部分截图
图5-5-2为hello.o的反汇编代码,图5-5-3为hello的反汇编代码。可以看到,在hello.o中跳转指令和call指令后为绝对地址,而在hello中已经是重定位之后的虚拟地址。
接下来,以0x4011f6处的call指令为例,说明链接过程:
正在上传…重新上传取消
图5-5-4查看hello.o的重定位信息
由上图可知,此处应该绑定第0xc个符号,同时链接器知道这里是相对寻址。接着查看hello.o的符号表,找到第12个符号puts,此处绑定puts的地址。
正在上传…重新上传取消
图5-5-5查看hello.o的符号表
在hello中找到puts的地址为0x401090。
正在上传…重新上传取消
图5-5-6 hello反汇编puts位置相关指令
当前PC的值为call指令的下一条指令的地址,也就是0x4011fb。而我们要跳转到的地方为0x401090,差0x16b,因此PC需要减去0x16b,也就是加上0xff ff fe 95,由于是小端法,因此重定位目标处应该填入 95 fe ff ff,如图:
正在上传…重新上传取消
图5-5-7 hello.o反汇编
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
子程序名 | 子程序地址 |
hello!_start | 0x00000000004010f0 |
hello!__libc_csu_init | 0x0000000000401270 |
hello!_init | 0x0000000000401000 |
hello!frame_dummy | 0x00000000004011d0 |
hello!register_tm_clones | 0x0000000000401160 |
hello!main | 0x00000000004011d6 |
hello!printf@plt | 0x0000000000401040 |
hello!atoi@plt | 0x0000000000401060 |
hello!sleep@plt | 0x0000000000401080 |
hello!getchar@plt | 0x0000000000401050 |
hello!exit@plt | 0x0000000000401070 |
hello!__do_global_dtors_aux | 0x00000000004011a0 |
hello!deregister_tm_clones | 0x0000000000401130 |
hello!_fini | 0x00000000004012e8 |
表5-6
5.7 Hello的动态链接分析
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而非静态链接一样把所有程序模块都链接成一个单独的可执行文件。
plt初始存的是一批代码,它们跳转到got所指示的位置,然后调用链接器。初始时got里面存的都是plt的第二条指令,随后链接器修改got,下一次plt就能跳转到正确的区域。延迟绑定是通过GOT和PLT实现的。GOT是数据段的一部分,而PLT是代码段的一部分。两表内容分别为:
PLT:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。
GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。
首先找到.got的地址为0x403ff0。
正在上传…重新上传取消
图5-7-1 查看.got的地址
在edb中找到相应地址,分析在dl_init前后,查看这些项的变化。
正在上传…重新上传取消
图5-7-2 dl_init前
正在上传…重新上传取消
图5-7-3 dl_init后
5.8 本章小结
本章首先介绍了链接的概念和作用,详细说明了可执行目标文件的结构及重定位过程。并且以可执行目标文件hello为例,具体分析了各个段、重定位过程、虚拟地址空间、执行流程等。
第6章 hello进程管理
6.1 进程的概念与作用
6.1.1进程的概念
进程的经典定义是一个执行中程序的实例,系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
每次用户通过向shell输入一个可执行目标文件的名字,运行程序时,shell会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,并且在这个新进程的上下文中运行它们自己的代码或其他应用程序。
6.1.2进程的作用
进程能够提供给应用程序的一些关键抽象,如:一个独立的逻辑控制流,它提供一个假象,好像程序能够独占使用处理器;一个私有的地址空间,它提供一个假象,好像程序能够独占使用内存系统。此外,通过进程可以更方便于描述和控制程序的并发执行,实现操作系统的并发性和共享性,更好实现CPU、时间、内存等资源的分配和调度。通过进程的使用可以做到在一个时间段内有多个程序并行,其中它们的资源均为独立分配,调度均为独立接受,运行也是独立的、互不打扰的。这样更方便实现各种控制与管理。
6.2 简述壳Shell-bash的作用与处理流程
6.2.1Shell-bash的作用:
Shell 是一个命令解释器,它解释由用户输入的命令并且把它们送到内核。Shell 有自己的编程语言用于对命令的编辑,它允许用户编写由 shell 命令组成的程序。 Shell 编程语言具有普通编程语言的很多特点,比如它也有循环结构和分支控制结构等,用这种编程语言编写的 Shell 程序与其他应用程序具有同样的效果。
6.2.2 Shell-bash的处理流程:
Shell 首先从命令行中找出特殊字符(元字符),在将元字符翻译成间隔符号。 将命令行划分成小块 tokens, 程序块 tokens 被处理,检查看他们是否是 shell 中所引用到的关键字。 tokens 被确定以后, shell 根据 aliases 文件中的列表来检查命令的第一个单词。如果这个单词出现在 aliases 表中,执行替换操作并且处理过程回到第一步重新分割程序块 tokens。 对~符号和所有前面带有$符号的变量进行替换, 并将命令行中的内嵌命令表达式替换成命令,多采用$(command)的方法进行标记。计算采用$(expression)进行标记的算术表达式。依据栏位分割符号将命令字符串重新划分为新的程序块。执行通配符的替换。把所有从处理结果中用到的注释删除,并且按照如下顺序进行命令的检查:内建命令,shell函数(由用户自己定义的),可执行的脚本文件(需要寻找文件以及PATH路径),初始化所有的输入输出重定向,执行命令。
6.3 Hello的fork进程创建过程
代参执行当前目录下的可执行文件(本作业中即为hello),父进程会通过fork函数创建一个新的运行的子进程hello。
子进程获取了父进程的上下文,包括栈信息、通用寄存器、程序计数器、环境变量和打开的文件相同的一份副本。两个进程本质上的不同就是子进程与父进程有着不同的PID。由于这种父进程与子进程的从属关系,子进程可以读取父进程打开的任何文件。
当子进程运行结束的时候,对其执行回收。若其父进程仍存在,则由其父进程执行回收,否则由init进程执行回收。
在这里,父进程为shell,在输入./hello的时候,首先shell会对我们输入的命令进行解析,shell会认为时执行当前目录下的可执行文件hello,因此shell会调用fork()创建一个子进程。
6.4 Hello的execve过程
execve过程发生在调用fork创建新的子进程之后。作用是在当前进程的上下文中加载并运行一个新的程序:hello。execve函数没有返回值,因为其没有返回这一过程。当且仅当出现错误的时候execve函数才会返回至调用程序。
子进程通过execve系统调用加载器删除子进程现有的虚拟内存段,并创建一组新的关于hello程序的代码、数据、堆和栈段。新的栈和堆段被初始化成0.通过将虚拟地址空间中的页映射到hello的页大小的片,新的代码和数据段被初始化为可执行文件的内容。最后跳转至hello的开始地址,开始执行main函数。
6.5 Hello的进程执行
6.5.1上下文信息
操作系统使用一种称为上下文切换的较高层次的异常控制流来实现多任务。其实上下文就是进程自身的虚拟地址空间,分为用户级上下文和系统及上下文。每个进程的虚拟地址空间和进程本身一一对应。由于每个CPU只能同时处理一个进程,而很多时候系统中有很多进程都要去运行,因此处理器只能一段时间就要切换新的进程去运行,而实现不同进程中指令交替执行的机制称为进程的上下文切换。
6.5.2进程时间片
一个进程执行它的控制流的一部分的每一时间段叫做时间片。
正在上传…重新上传取消
图6-5-1 上下文切换机制
如上图所示,为进程A与进程B之间的相互切换。处理器通常使用一个寄存器提供两种模式的区分,该寄存器描述了进程当前享有的特权,当没有设置模式位时,为用户模式;设置模式位为内核模式。用户模式就是运行相应进程的代码段的内容,此时进程不允许运行特权指令,也不允许直接引用地址空间中内核区内的代码和数据;而内核模式中,进程可以运行任何指令。
6.6 hello的异常与信号处理
hello执行中可能出现的异常:
(1)中断:异步发生的。在执行hello程序的时候,由处理器外部的I/O设备的信号引起的。I/O设备通过像处理器芯片上的一个引脚发信号,并将异常号放到系统总线上,来触发中断。这个异常号标识了引起中断的设备。在当前指令完成执行后,处理器注意到中断引脚的电压变高了,就从系统总线读取异常号,然后调用适当的中断处理程序。在处理程序返回前,将控制返回给下一条指令。结果就像没有发生过中断一样。
(2)陷阱:陷阱是有意的异常,是执行一条指令的结果,hello执行sleep函数的时候会出现这个异常。
(3)故障:由错误引起,可能被故障处理程序修正。在执行hello时,可能出现缺页故障。
(4)终止:不可恢复的致命错误造成的结果,通常是一些硬件错误,比如DRAM或者 SRAM位被损坏时发生的奇偶错误。
当输入回车或随机字符串时
正在上传…重新上传取消
图6-6-1:异常——输入回车、随机字符串
可以发现回车和随机字符串对于程序的运行并没有影响,终端中会出现所有相关的输入,此外如果随机字符串搭配回车一起输入,每个回车会被终端识别为一条新的指令,并不会影响到现有程序运行。
输入Ctrl-C的时候
正在上传…重新上传取消
图6-6-2:异常——输入Crtl-C
当向hello程序输入Ctrl-C后,会导致中断异常产生SIGINT信号,向子进程发出SIGKILL信号终止并回收,进程会终止。
输入Crtl-Z的时候
正在上传…重新上传取消
图6-6-3:异常——输入Ctrl-Z
当向hello程序输入Ctrl-Z后,会导致中断异常产生SIGSTP信号,进程被挂起,与输入Ctrl-C结果不同。此时分别输入ps,jobs,pstree,fg,kill指令进行查看相关信息:
正在上传…重新上传取消
图6-6-4:异常处理——ps指令
可以看出hello进程并未停止,而是被挂起。在进程列表中仍然可以看到hello这个进程。究其原因是SIGTSTP是一个暂时停止的信号,如果该进程接收到一个SIGCONT信号就会继续运行。故该进程只是被挂起,而并没有如上文所提被父进程或者是init进程回收。
正在上传…重新上传取消
图6-6-5:异常处理——jobs指令
通过本指令验证hello进程确实被挂起,处于停止的状态。
正在上传…重新上传取消
图6-6-6:异常处理——pstree指令
可以通过调出进程树来查看所有进程的情况。
正在上传…重新上传取消
图6-6-7:异常处理——fg指令
fg指令的用处是使第一个后台作业变成前台作业,正巧这里hello是第一个后台作业,所以fg会使得hello回到前台并完成运行。
正在上传…重新上传取消
图6-6-8:异常处理——kill指令
kill指令成功杀死进程hello并输出相应提示。
6.7本章小结
本章从进程的管理来认识hello。我们从进程的概念、作用,以及我们操纵进程的shell的处理流程入手,了解了进程的相关工作原理。而后我们分析了与进程息息相关的两个函数——fork和execve的作用和工作机理,并把这些都应用到了对于hello进程的管理之中。我们看到了发送不同的异常信号以及不同的针对SIGTSTP信号的异常解决方法,使我们的理论知识得到了验证。通过这一章,我们对于hello程序的进程管理有了更具体的认识。
第7章 hello的存储管理
7.1 hello的存储器地址空间
(以下格式自行编排,编辑时删除)
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
7.2 Intel逻辑地址到线性地址的变换-段式管理
(以下格式自行编排,编辑时删除)
7.3 Hello的线性地址到物理地址的变换-页式管理
(以下格式自行编排,编辑时删除)
7.4 TLB与四级页表支持下的VA到PA的变换
(以下格式自行编排,编辑时删除)
7.5 三级Cache支持下的物理内存访问
(以下格式自行编排,编辑时删除)
7.6 hello进程fork时的内存映射
(以下格式自行编排,编辑时删除)
7.7 hello进程execve时的内存映射
(以下格式自行编排,编辑时删除)
7.8 缺页故障与缺页中断处理
(以下格式自行编排,编辑时删除)
7.9动态存储分配管理
(以下格式自行编排,编辑时删除)
Printf会调用malloc,请简述动态内存管理的基本方法与策略。
7.10本章小结
(以下格式自行编排,编辑时删除)
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
(以下格式自行编排,编辑时删除)
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
(以下格式自行编排,编辑时删除)
8.3 printf的实现分析
(以下格式自行编排,编辑时删除)
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
(以下格式自行编排,编辑时删除)
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
(以下格式自行编排,编辑时删除)
(第8章1分)
结论
本文讲述了hello的一生:
键盘敲下代码,得到hello.c源文件,hello从此而生。
预处理阶段:预处理器(cpp)根据以字符#开头的命令,修改原始C程序,生成hello.i。
编译阶段:编译器(ccl)将文本文件hello.i 翻译成文本文件hello.s,它包含一个汇编语言程序。
汇编阶段:接下来,汇编器(as)将hello.s 翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o 中。
链接阶段:链接器(ld)合并标准C 库和hello.o 生成可执行文件hello。
运行程序:利用I/O的管理机制,在终端输入./hello 2021110800 dhk 1。shell进程调用fork为hello创建一个子进程,随后调用execve启动加载器,加载映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入main函数。
执行:CPU为其分配时间片,在一个时间片中hello享有CPU资源,顺序执行自己的控制逻辑流。
内存:MMU 将程序中使用的虚拟内存地址通过页表映射成物理地址。printf 会调用 malloc 向动态内存分配器申请堆中的内存。
进程结束:shell父进程回收子进程,hello的一生到此结束。
hello的一生很短,短到我们几句话就可以叙述完;hello的一生也很长,长在每句话中都蕴含着丰富的知识与计算机底层的原理。可以说,hello的一生是璀璨且精彩的,hello带着我又重新了解的计算机系统的魅力,计算机系统的基本概念,包括最底层的内存中的数据表示、流水线指令的构成、虚拟存储器、编译系统、动态加载库等等,让我知道计算机并不是那么简单的东西,而是由很多内容构成,丰富多彩的,今后我也会继续学习有关计算机的知识,逐渐发现计算机的奥秘!
附件
列出所有的中间产物的文件名,并予以说明起作用。
hello.i:hello.c预处理后的文件。
hello.s:hello.i编译后的文件。
hello.o:hello.s汇编后的文件。
hello:hello.o链接后的文件。
hello1asm.txt:hello.o反汇编后代码。
hello2asm.txt:hello反汇编后代码。
hello.o.elf:hello.o用readelf -a hello.o指令生成的文件。
hello.elf:hello用readelf -a hello指令生成的文件。
参考文献
[1] Randal E. Bryant;David R. O’Hallaron. 深入理解计算机系统. 北京:机械工业出版社
[2] 一个简单程序从编译、链接、装载(执行)的过程-静态链接