Hello的一生
第1章 概述
1.1 Hello简介
之后的阶段,将文件名输入到名为shell应用程序,shell会加载并运行我们的hello,具体为,利用fork得到一个与父进程几乎相同的子进程,并且分配相对应的内存资源,虚拟内存,CPU的使用权限,shell为他execve,调用加载器,并映射虚拟内存,之后跳转到程序入口,调用main函数,CPU通过取值、译码、执行操作来实现程序,并按照进程切换方式工作。当程序运行结束后,其处于终止状态,父进程对hello进行回收,删除相应的数据,hello从此消失,不带走一片云彩。这就是020。
1.2 环境与工具
1.2.1 硬件环境
X64CPU 2.80GHZ 8.0G RAM 1TB HD Disk 1.2.2
1.2.2 软件环境
Windows 10 X64位 Vmware14 Ubuntu16.04LTS64位
Ubuntu下GDB、edb 、readelf、vim
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
文件 | 作用 |
---|---|
hello.i | hello.c文件预处理后的文件 |
hello.s | hello.i文件经过编译之后得到的文件 |
hello.o | hello.s汇编之后得到的可重定位目标文件 |
helloo.elf | hello.o的elf格式文件 |
hello.c | hello.c的C语言文件 |
hello.elf | hello可执行目标文件的elf格式文件 |
hello | hello.c经过编译之后得到的可执行目标文件 |
1.4 本章小结
本章主要描述了hello从生到死传奇的一段“程”生,尽管只是笼统的介绍,但也算对他的生平有了一定的了解。同时本章还说明了本次作业的环境和中间结果的作用。
第2章 预处理
2.1 预处理的概念与作用
预处理的概念:预处理器(cpp)根据以字符“#”开头的命令,修改原始的C程序。
作用:处理代码中以#开头的预编译指令、删除注释等
例如:预处理会对下面的带#的指令进行文字描述的操作
#include 包含一个源代码文件
#define 定义宏
#undef 取消已定义的宏
#if如果给定条件为真,则编译下面代码
#ifdef 如果宏已经定义,则编译下面代码
#ifndef如果宏没有定义,则编译下面代码
#elif 如果前#if条件不为真,当前条件为真,则编译下面代码,其实就是else if的简写
#endif结束一个#if……#else条件编译块
#error停止编译并显示错误信息
2.2在Ubuntu下预处理的命令
gcc -E hello.c -o hello.i
2.4 本章小结
本章主要是对hello.c的生命开端的描述,介绍了其p2p最开始的步骤,预处理,描述了预处理的概念作用以及在linux下的指令,并将预处理之后的hello.i文件和hello.c进行了比较。
第3章 编译
3.1 编译的概念与作用
编译的作用:编译的作用就是将一个.i文件通过一系列的分析优化,生成相应的汇编语言程序以进行后续的操作。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
hello.c中还有一个局部变量i 其应该存放在栈中在hello.s中如下描述
3.3.2赋值
1、将sleepsecs赋值为2.5,直接在hello.s开头进行如下图操作
3.3.3 类型转换
在hello.c中隐式的将float类型的2.5转换为整型,遵从向0舍入,如下图
3.3.5关系操作
在hello.c的for循环中存在一个i与10的比较 判断i
3.3.6数组操作
在hello.c中有对第二个参数即数组的操作,在hello.s中通过如下进行访问,红线是获取地址,黑线是获取值
调用函数:
调用pus函数的指令
调用sleep函数
4.3 可重定位目标elf格式
节头表包含了文件中出现的节的类型位置和大小。
其中对于重定位.rela.text我们重点分析,他是一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,修改这些位置。
4.4 Hello.o的结果解析
5.3 可执行目标文件hello的格式
我们以hello.o中的一个需要重定位的内容进行举例。
对于我们的hello我们通过5.3小节,知道它的.got地址是0x6008b8,查看他的信息发现全是0
6.4 Hello的execve过程
在shell fork了一个子进程之后,这个子进程会调用execve函数在当前进程的上下文中加载并运行hello程序。execve函数加载并运行可执行目标文件hello,且带参数列表argv和环境变量列表envp。只有当出现错误时,例如找不到hello,execve才会返回到调用程序。所有与fork一次调用返回两次不一样,execve调用一次从不返回。在execve加载了hello,他调用启动代码,启动代码设置栈,并将控制传递给新程序主函数。在main开始执行时,用户栈的组织结构如下图
6.5 Hello的进程执行
操作系统内核使用一种称为上下文切换的异常控制流来实现多任务,所谓上下文就是内核重新启动一个被抢占的进程所需的状态。当hello没有被别的进程抢占时,也就是他处于自己的进程时间片,此时按照hello程序顺序执行hello,当有别的进程进行抢占时,就会发生上下文切换,内核进行调度,保存hello进程上下文恢复一个之前被抢占的进程上下文,并将控制转给这个进程。具体如下图
2、在执行过程中按下Ctrl-C
4、 在执行过程中按下Ctrl-Z,发送一个停止信 将hello挂起
运行fg指令,发送SIGCONT信 继续执行程序
6.7本章小结
本章我们主要介绍了已经出生了的hello是如何被执行的,shell是如何为他fork一个子进程,子进程是如何为他execve。我们还介绍了进程之间是如何切换的,最后我们还测试了一些信 和异常情况,通过在命令行输入指令来实现了对一个进程的控制。
第7章 hello的存储管理
7.1 hello的存储器地址空间
物理地址:计算机系统的主存被组织成一个由M个连续字节大小的单元组成的数组。每字节都有一个唯一的物理地址。CPU通过地址总线寻址就是物理地址。
线性地址:地址空间是一个非负整数地址的有序集合,如果地址空间中的整数是连续的,那么我们就说他是一个线性地址空间,在这里是和hello的虚拟地址空间相同
虚拟地址:在一个带虚拟内存的系统,cpu从一个有N个地址的地址空间中生成虚拟地址,也就是虚拟内存中的地址。
逻辑地址:是在有地址变换功能的计算机中,访内指令给出的地址 (操作数) 叫逻辑地址,也叫相对地址,也就是是机器语言指令中,用来指定一个操作数或是一条指令的地址。要经过寻址方式的计算或变换才得到内存储器中的实际有效地址即物理地址。一个逻辑地址由两部份组成,段标识符: 段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是个索引 ,后面3位包含一些硬件细节 。
7.2 Intel逻辑地址到线性地址的变换-段式管理
一个逻辑地址由两部分组成,段标识符:段内偏移量。段标识符是由一个16位子长字段组成,称为段选择符,前13位是一个索引 ,后3位包含硬件细节,如下图。
在intel设计,一些全局段描述符放在“全局段描述符表中(GDT)”,局部的,例如每个进程自己的,放在局部段描述符表LDT,通过段选择符中的T1字段=0表示GDT=1表示LDT。
具体变化过程为:
看段选择符T1是0还是1,知道是GDT还是LDT中的段,根据相应寄存器得到地址和大小。
通过段选择符前13位找到相应段描述符,查找BASE得到基地址
把BASE+OFFSET得到线性地址
而翻译地址过程是由MMU来进行的。CPU中一个控制寄存器,页表基址寄存器指向当前页表。一个n位虚拟地址包含两个部分,1个p位的虚拟页面偏移,一个N-p位的虚拟页 。MMU利用VPN也就是虚拟页 来选择适当的PTE,将页表条目中的物理页 和虚拟地址中的VPO串联起来,就得到了相应的物理地址。下图描述了整个过程。
7.6 hello进程fork时的内存映射
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面。
7.8 缺页故障与缺页中断处理
当CPU引用虚拟内存中的一个页的某个字时,,地址翻译硬件从内存中读取相应的PTE,从有效位推断这个页是否被缓存,如果未被缓存,则会触发一个缺页异常。缺页异常调用内核中的缺页处理程序,该程序会选择牺牲一个页,并修改牺牲页的页表条目。接下来,内核从磁盘复制这个读取页到内存中,更新相应的PTE随后返回。
当异常处理程序返回后,他会重新启动导致缺页的指令,该指令会把导致缺页的虚拟地址重发送到地址翻译硬件。
具体过程如下图所示
隐式链表的优点是简单,显著缺点是任何操作开销要对空闲链表搜索,搜索时间与堆中已分配块和空闲块总数呈线性关系。
在合并时,有四种情况,根据不同情况修改相应的头和脚就可以实现合并。
显示空闲链表:
将堆组织成一个双向空闲链表,在每个空闲块中,都包含一个pred前驱和succ后继指针。

使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。不过,释放一个块的时间可以是现行的,也可能是个常数。
一种方法是后进先出的顺序维护链表,将新释放的块放置在链表的开始处。使用LIFO的顺序和首次配适的放置策略,分配器会最先检查最近使用过的块。
另一种是按照地址顺序来维护链表,其中链表中每个块的地址都小于它后继的地址。在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。
一般而言,显示链表的缺点是空闲块必须足够大,以包含所有需要的指针,以及头部和可能的脚部。这就导致了更大的最小块大小,也潜在地提高了内部碎片程度
7.10本章小结
本章是十分重要且复杂的,主要分析了hello的存储,对此我们介绍了一些基本地址空间的知识,同时也对一些地址间的转换方法做了介绍,分析了在得到地址后如何进行数据的读取,以及fork和execve如何进行内存映射,以及动态分配内存的方法。可以看到我们的hello真的是一个重要的角色,这么多规则都要围绕他转。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
所有的I/O设备(例如 络、磁盘和终端)都被模型化为文件,而所有的输入和输出都比当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,成为Unix I/O 这使得所有的输入和输出都能以一种统一且一致的方式来执行。
8.2 简述Unix IO接口及其函数
Unix I/O接口:
1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访间一个I/O 设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
2.Linux shell 创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0) 、标准输出(描述符为1) 和标准错误(描述符为2) 。头文件 定义了常量STDIN_FILENO 、STOOUT_FILENO 和STDERR_FILENO, 它们可用来代替显式的描述符值。
3.改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k, 初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek 操作,显式地设置文件的当前位置为k 。
4.读写文件。一个读操作就是从文件复制n>0 个字节到内存,从当前文件位置k 开始,然后将k增加到k+n 。给定一个大小为m 字节的文件,当k>=m 时执行读操作会触发一个称为end-of-file(EOF) 的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF 符 ” 。
类似地,写操作就是从内存复制n>0 个字节到一个文件,从当前文件位置k开始,然后更新k 。
5.关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。
Unix I/O函数:
1、通过调用open函数打开一个已存在的文件或者创建一个新文件
int open(char* filename, int flags ,mode_t mode)
open函数将文件转为文件描述符,返回描述符数字。返回的描述符总是进程中当前没有打开最小。Flag参数,指明如何访问文件也可是一个或多为掩码的或。
O_RDONLY:只读
O_WRONLY:只写
O_RDWR:可读可写
Mode参数指定了新文件的访问权限位。若返回-1则出错。
2、ssize_t read(int fd, void*buf ,size_t n)
Read函数从描述符为fd的当前文件位置复制最多n个字节到内存buf。返回值为-1表示一个错误,返回0表示EOF,否则返回值表示的是实际传送字节数。
3、ssize_t write(int fd, const void *buf, size_t n);
write 函数从内存位置buf 复制至多n 个字节到描述符fd 的当前文件位置。若成功则返回写的字节数,若出错则为-1。
8.3 printf的实现分析
首先来看printf的代码
可以看到在参数部分采用了一个可变形参的写法,并且在下面这条语句va_list arg = (va_list)((char*)(&fmt) + 4); 获取了…中的第一个参数。接下来调用了vsprintf函数
我们查看这个函数的代码
int vsprintf(char *buf, const char *fmt, va_list args){ int len; int i; char * str; char *s; int *ip; int flags; /* flags to number() */ int field_width; /* width of output field */ int precision; /* min. # of digits for integers; max number of chars for from string */ int qualifier; /* 'h', 'l', or 'L' for integer fields */ for (str = buf; *fmt; ++fmt) { if (*fmt != '%') { *str++
声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!