河曲智叟 发表于 2025-3-13 06:34:25

计算机体系大作业程序人生-Hello’s P2P-2023


摘  要
在编译源文件的过程中,gcc通过调用cpp/cc1/as/ld,将C语言源文件举行预处理、编译、汇编、链接,最终形成可实行目标文件hello,由存储器生存在磁盘中。运行进程时,操作体系为其分配虚拟地点空间,提供非常控制流等强盛的工具,Unix I/O为其提供与程序员和体系文件交互的方式。本文通太过析Hello程序从代码编辑器到运行进程的过程,对计算机体系编译源文件、运行进程等机制举行较深入的分析和先容。

关键词:计算机体系;操作体系; C语言底层实现;CSAPP;


目  录

第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简介

P2P指的是从Program(文本程序)到Process(进程)的过程,具体颠末了如下的步骤:

https://i-blog.csdnimg.cn/blog_migrate/6ada0545a09ab73de0729d15ebdc543b.png
 
图编译流程

[*]由我们编写hello.c源文件,并举行编译。
[*]Gcc(或其他编程语言编译器)将.c后缀的文本文件颠末cpp预处理器举行预处理,根据#开头的命令,修改源程序(例如#include<stdio.h>将stdio.h中的内容归并到源文件中),生成新的.i文件。
[*]再通过编译器ccl,将.i文件编译成.s的汇编程序。
[*]接着颠末编译器as将.s文件翻译成呆板语言,生成可重定位目标程序,一个二进制文件,但此时还不可运行。
[*]链接:将标准C库中的printf.o等函数与可重定位目标程序归并,得到二进制的可实行文件。
020是指从0到0的过程,在hello没有实行的时间是0,hello结束后仍是0:

[*]运行:输入hello 2021112846 唐葳蕤 3
[*]创建子程序:壳(Shell)通过fork创建子程序hello成为进程的开始,也就是process的开始;
[*]实行:通过execve加载器载入,建立虚拟内存映射,设置当前进程的上下文中的程序计数器,使之指向程序入口处。CPU为其分配相应的时间分片,形成同其他程序并发实行的假象;
[*]访存:CPU上的内存管理单元MMU根据页表将CPU生成的虚拟地点翻译成物理地点,将相应的页面调理;
[*]动态内存申请:printf调用malloc举行动态内存分配,在堆中申请所需的内存;
[*]接收信号:中途担当Ctrl+Z挂起,Ctrl+C终止等信号;
[*]结束:程序返回后,内核向父进程(Shell)发送SIGCHLD信号,此时终止的hello被父进程接纳。
在实行hello程序时,MMU根据TLB将VA翻译成PA,向Cache发出请求,颠末一个个缺页故障后,hello被逐层载入物理内存,就如许,hello脱离磁盘,通过I/O桥,一生的观光就开始了。操作体系提供非常控制流等强盛的工具,不断对体系中运行着的进程举行调理。Unix I/O为其提供与程序员和体系文件交互的方式,让它不再孤单。当程序完成了它的任务,main函数返回后,程序就死去了。之后,Shell作为其父进程会负责将其接纳,操作体系内核删除相关数据结构,释放其占据的资源,hello从0到0的一生就此结束。
1.2 情况与工具

1.2.1 硬件情况

Intel(R) Core(TM) i7-1065G7 CPU @ 1.30GHz 1.50GHz,
16.0GB RAM, 512GB SSD
1.2.2 软件情况

Windows10 64位;Vmware Workstation 17.0;KUbuntu 64位;
1.2.3 开发工具

Visual Studio 2022 64位;Codeblocks;Visual Studio Code;vim/kate+gcc
1.3 中间结果

文件作用
文件名
预处理后的文件
hello.i
编译之后的汇编文件
hello.s
汇编之后的可重定位目标文件
hello.o
链接之后的可实行目标文件
hello
hello.o的ELF格式
elf__hello_o.txt
hello.o的反汇编代码
dump__hello_o.txt
hello的ELF格式
elf__hello.txt
hello的反汇编代码
dump__hello.txt

1.4 本章小结

本章对hello的一生举行了简要的先容和描述,先容了P2P(From Program to Process)及020(From Zero-0 to Zero-0)的过程,先容了本计算机的硬件情况、软件情况、开发工具,先容了为编写本论文的中间文件的名称和其作用。还列出了为完本钱次大作业,生成的中间结果文件的名字以及文件的作用。



第2章 预处理

2.1 预处理的概念与作用

2.1.1预处理是什么
程序设计领域中,预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。
典型地,由预处理器(preprocessor) 对程序源代码文本举行处理,得到的结果再由编译器核心进一步编译。这个过程并不对程序的源代码举行解析,但它把源代码分割或处理成为特定的单位——预处理记号(preprocessing token)用来支持语言特性(如宏调用)。
最常见的有预处理操作的是C语言和C++语言。预处理器在UNIX传统中通常缩写为PP,在自动构建脚本中C预处理器被缩写为CPP的宏指代。为了不造成歧义,C++(c-plus-plus) 常常并不是缩写为CPP,而改成CXX。
2.1.2为什么需要预处理?
ISO C和ISO C++都规定程序由源代码被翻译分为若干有序的阶段(phase),通常前几个阶段由预处理器实现。预处理中会展开以#起始的行,试图表明为预处理指令(preprocessing directive) ,此中ISO C/C++要求支持的包罗#if / #ifdef / #ifndef / #else / #elif / #endif(条件编译)、#define(宏定义)、#include(源文件包含)、#line(行控制)、#error(错误指令)、#pragma(和实现相关的杂注)以及单独的#(空指令)。预处理指令一般被用来使源代码在差别的实行情况中被方便的修改大概编译。
2.2在Ubuntu下预处理的命令

https://i-blog.csdnimg.cn/blog_migrate/93cba8b4e99654b80baf296334b6565f.png

图2.1 cpp预处理命令
运行后的结果定向输出到了hello.i中
2.3 Hello的预处理结果解析



2.3.1文件增大

https://i-blog.csdnimg.cn/blog_migrate/fb4875fbbe8b1d6191c150a0955a2a56.png
 
图2.2 hello.i行数
可以发现,整个.i文件相比于.c文件,扩展到了三千多行。
2.3.2源代码保存

https://i-blog.csdnimg.cn/blog_migrate/466bef3f26a4c1ab6fce82e181418c3c.png
 
图2.3 hello.i中原样保存的代码
hello.c程序本来的内容存放在了最后。而在源代码前面的则是stdio.h、unistd.h、stdlib.h文件中代码的依次展开。

2.3.3模块化拼接

https://i-blog.csdnimg.cn/blog_migrate/5193bcc48da631ecbdf157a72074726e.png
 
图2.4 hello.i中开头存放的拼接用到的库文件名称
最开始的一段代码,是拼接用到的各种库文件。

https://i-blog.csdnimg.cn/blog_migrate/224804a07223bc22fc9951020dd8411d.png
 
图2.5 hello.i中间某段代码
中间的某段代码,是对很多内部的函数举行声明。

https://i-blog.csdnimg.cn/blog_migrate/7ec934efd70d617ad868a6ffb995c73e.png
 
图2.6 hello.i中原样保存的代码
程序的源代码放在了文件的末尾。
2.3.4注释删除
程序中包含的注释被全部删除了。
2.4 本章小结

本章先容了预处理的概念和作用,学习了在用cpp指令对hello.c文件举行预处理,将其重定向到hello.i中。我们欣赏了hello.i的代码,对hello.i的内容有了感性认识;明白了预处理过程只是做些代码文本的替换工作,是为编译做的预备工作的阶段,对源程序并没有举行语法查抄、修改等操作。这是我们从.c源程序得到可实行文件的第一步。

第3章 编译

3.1 编译的概念与作用

现在,hello.c颠末了代码的拼接,已经和引用的文件归并,拥有了完备的代码。不外,离计算机可以“阅读”的二进制代码还任重道远。我们需要设法将它转变为可实行目标文件。而起首,我们需要把它转化为底层的汇编语言。
3.1.1什么是编译?
编译程序所要做的工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。
3.1.2为什么要编译?
程序通过编译可以起到如下作用:

[*]语法分析:编译程序的语法分析器以单词符号作为输入,分析单词符号串是否形成符合语法规则的语法单位,方法分为两种:自上而下分析法和自下而上分析法。
[*]中间代码:源程序的一种内部表示,或称中间语言。中间代码的作用是可使编译程序的结构在逻辑上更为简单明白,特殊是可使优化目标代码比力轻易实现的中间代码。
[*]代码优化:指对程序举行多种等价变换,使得从变换后的程序出发,生成更有效的目标代码。
[*]目标代码:生成是编译的最后一个阶段。目标代码生成器把语法分析后或优化后的中间代码变换成目标代码。此处指汇编语言代码,须颠末汇编程序汇编后,成为可实行的呆板语言代码。
3.2 在Ubuntu下编译的命令


https://i-blog.csdnimg.cn/blog_migrate/9caeac280de1b69c83b1fc77b61fae68.png
 
图3.1 gcc生成汇编文件命令
3.3 Hello的编译结果解析

3.3.1 C语言和汇编代码的main函数总览

https://i-blog.csdnimg.cn/blog_migrate/da19cc08ea2b291204ada20f8252a217.png
 
图3.2 C语言的main函数
 
https://i-blog.csdnimg.cn/blog_migrate/387061d2cea0207b06509f49ae5ea868.png
 

图3.3 汇编后的main函数


3.3.2数据和赋值
1.整数和字符串常量
在语句https://i-blog.csdnimg.cn/blog_migrate/c60a3d0918de7961ec935be289c4e00d.png中,常量3的值生存的位置在.text段(代码)中,作为指令的一部分:https://i-blog.csdnimg.cn/blog_migrate/6edf964b211b58c982ca5fd477a968ba.png
 
 
同理,语句https://i-blog.csdnimg.cn/blog_migrate/8121970782268a80cccc6b466bdbe756.png中的数字0和5也被存储在.text段中:
 
https://i-blog.csdnimg.cn/blog_migrate/b970b5fd9b04706e4475855540279584.png
 

而在这两个printf函数中:
https://i-blog.csdnimg.cn/blog_migrate/2066bcde2daeebf176bdde702941f02c.png
 https://i-blog.csdnimg.cn/blog_migrate/f7966bb6998b5d204d4e8f14db3502d1.png
 


printf()中的字符串常量被存储在了.rodata节中:
https://i-blog.csdnimg.cn/blog_migrate/c6f9f97ee70c89868ea41ff4076ad265.png
 

2.变量
全局变量:
已经初始化并且初始值非零的全局变量储存在.data节,它的初始化不需要汇编语句,而是通过虚拟内存请求二进制零的页直接完成的。在本次的hello程序中没有用到。
局部变量:
局部变量存储在寄存器或栈中。例如程序中的局部变量 https://i-blog.csdnimg.cn/blog_migrate/22449bb6ba4da09380cad5c47027a6ad.png,在汇编代码中体现为,这表示i被生存在了栈中,且位置为%rsp-4处。
 

3.算术操作
在for循环中,使用了++操作符:https://i-blog.csdnimg.cn/blog_migrate/a4bc4bdc2268508900776fba93da16f8.png,与其对应的汇编代码为:
 
https://i-blog.csdnimg.cn/blog_migrate/24318a028f0e760e5be40fe867ddd7a3.png表示对i自加,栈上存储i的内存处的的值加1。
 

4.关系操作和控制转移
在开头,程序判断传入参数argc是否等于4,https://i-blog.csdnimg.cn/blog_migrate/d47c47fccf36a9924812ae3b94b052ed.png
 
与其对应的汇编代码为:https://i-blog.csdnimg.cn/blog_migrate/f8b9433a38b4de75c06690e112d5972b.png。
 
此中je语句用于判断cmpl比力产生的状态码,如果cmpl中的两个操作数的值不相称则不跳转,次序实行(即退出),而若相称则继续程序。
在程序中间的for循环中的循环条件为:https://i-blog.csdnimg.cn/blog_migrate/5170aa1150336ece75e27420749b994e.png
 
对应的汇编代码为:https://i-blog.csdnimg.cn/blog_migrate/c6da3450a72af633984ac387f190983c.png
 

这段代码接纳了init-跳转到中间-循环判断的模式。此中jle用于判断cmpl产生的条件码,若后一个操作数的值小于等于前一个,则跳转到.L4——继续实行循环,否则退出循环继续实行后面的代码。

5.数组/指针/结构操作
主函数main的参数中有指针数组char *argv[]:https://i-blog.csdnimg.cn/blog_migrate/781bd0eb8d7e05b575109bee824fef2a.png
 

在argv数组中,第n个指针指向的就是传入的第n个参数字符串的首地点。此中argv指向被运行的程序的路径,argv、argv 和argv分别表示输入的学号、名字和秒数。
因为char* 为指针范例,程序是64位的,以是每个char*指针占据8个字节,根据汇编代码: https://i-blog.csdnimg.cn/blog_migrate/7d51a01859568a261cb368ff3bae5773.png以及:
https://i-blog.csdnimg.cn/blog_migrate/517a6e6f92a424d65d762e6532e00bde.png
 
 

对比原函数可知,M[%rbp-32+8]、M[%rbp-32+16] 和M[%rbp-32+24]分别为argv、argv 和argv三个字符串的首地点。

3.3.3函数操作
在X86-64体系中,约定传递的参数中第1~6个参数依次储存在%rdi、%rsi、%rdx、%rcx、%r8、%r9这六个寄存器中,剩下的参数生存在栈当中。
1.main函数
参数传递:传入参数argc和argv[],分别用寄存器%rdi和%rsi存储。
函数调用:被体系启动函数调用。
函数返回:设置%eax为0并且返回,对应return 0 。
源代码:https://i-blog.csdnimg.cn/blog_migrate/323b1cbee996a1f54da1e9969b65ae87.png
 
对应的汇编代码:https://i-blog.csdnimg.cn/blog_migrate/e0c7f32fcf1cc6a2a4c3bd513473b741.png
 

从汇编代码中可以看出,main函数将传入的两个存放在edi和rsi中的参数分别存放进了M[%rbp-20]、M[%rbp-32]中。


2.printf函数
a.) puts
为什么在C语言中写的是printf,但编译之后调用的却是puts函数呢?颠末查阅资料,我们发现在输出固定的常量字符串时,puts函数的开销小于printf,以是编译器自动做了这个优化。
参数传递:传入了要输出的字符串的首地点。
函数调用:if条件满足后调用(或cmpl和je的条件)
源代码:https://i-blog.csdnimg.cn/blog_migrate/b9da3d2d5472ebab30b82e09c4506e33.png
 
汇编代码:https://i-blog.csdnimg.cn/blog_migrate/ba059b440f556adc9e6f7be92d359eba.png
 

b. )for循环中
参数传递:传入了格式字符串的地点、 argv、argv的地点。
函数调用:在for循环过程中调用。
源代码:https://i-blog.csdnimg.cn/blog_migrate/16ad12da1bb2a4e350f7d108ed75b069.png
 
汇编代码:https://i-blog.csdnimg.cn/blog_migrate/224f7864e4d5b70f861286a7bb11a889.png
 


3.exit函数
参数传递:传入立刻数1。
函数调用:if条件满足后调用(或cmpl和je的条件)。
源代码:https://i-blog.csdnimg.cn/blog_migrate/ea62837a1f238e2a8cd8770fb3a6fb66.png
 
汇编代码:https://i-blog.csdnimg.cn/blog_migrate/8c6b290de36a283183d7afb749a8485d.png
 

4.atoi函数
参数传递:argv的地点。
函数调用:在for循环过程中调用。
函数作用:将字符串转化为数字(int)。
源代码:https://i-blog.csdnimg.cn/blog_migrate/62818a4fa688513a3adc9da5ffd93d97.png
 
汇编代码:
https://i-blog.csdnimg.cn/blog_migrate/7d40b23d10a87097297a960583406d33.png
 

5.sleep函数
参数传递:argv颠末atoi转化后的数字
函数调用:在for循环过程中调用。
源代码:https://i-blog.csdnimg.cn/blog_migrate/3a7b0311ae1c565f9ab829737595cd9a.png
 
汇编代码:https://i-blog.csdnimg.cn/blog_migrate/e2568de80c7051bd04b639f366a47e11.png
 

6.getchar函数
函数调用:在main中被调用
源代码:https://i-blog.csdnimg.cn/blog_migrate/2a2853bfb0bc373f425cbcfc9cedd696.png
 
汇编代码:https://i-blog.csdnimg.cn/blog_migrate/f9cb56206748717bdb24b6c10b620ff4.png
 
3.4 本章小结

本章先容了编译的概念以及过程。通过hello程序分析了编译器如何将c语言转换成为汇编代码。先容了汇编代码如何实现变量、常量、传递参数以及分支和循环。进一步深化了我们对C语言中的数据与操作的认识。

第4章 汇编

4.1 汇编的概念与作用

现在hello程序已经转换为了汇编代码,但是汇编代码仍然不是计算机可以读懂的语言。究竟CPU固然速度快功能多,但其根本还是逻辑电路,只能实行二进制指令。那么我们现在就来看看,如何将hello.s文件汇编为二进制的hello.o文件。
4.1.1什么是汇编
驱动程序运行汇编器as,将汇编语言的ascii码文件(这里是hello.s)翻译成呆板语言的可重定位目标文件(hello.o)的过程称为汇编。

4.1.2为什么要汇编
.o文件是一个二进制文件,它包含程序的指令编码。
汇编就是将高级语言转化为呆板可直接辨认实行的代码文件的过程,汇编器将.s 汇编程序翻译成呆板语言指令,把这些指令打包成可重定位目标程序的格式,并将结果生存在.o目标文件中。
4.2 在Ubuntu下汇编的命令

https://i-blog.csdnimg.cn/blog_migrate/0b007dc14c7505109aa1cae816bd750f.png
 
图4.1 as命令
4.3 可重定位目标elf格式

4.3.1 readelf命令
https://i-blog.csdnimg.cn/blog_migrate/3ba27e333277ce0b8e85b5a9da048729.png
 
图4.2 readelf命令
运行后的结果定向输出到了elf__hello_o.txt中
4.3.2 ELF头
ELF头中包含了体系信息,编码方式,ELF头巨细,节的巨细和数目等等一系列信息。ELF头的具体内容如下:
https://i-blog.csdnimg.cn/blog_migrate/32c15ed49e15dd481e2cedb21418234f.png
 
图4.3 ELF头
4.3.3  节头目表
节头目表描述了.o文件中出现的各个节的范例、位置、所占空间巨细等信息。
https://i-blog.csdnimg.cn/blog_migrate/ec45d7000f17323afa4f6688705fbe3d.png
 
图4.4 节头目表

4.3.4  重定位节
重定位节记录了各个段中在链接时需要通过重定位对其地点举行修改的外部符号等内容。链接器会通过重定位节的重定位条目计算出正确的地点。
hello.o中需要重定位的内容:.rodata中的模式串,puts,exit,printf,atoi,sleep,getchar等符号。
https://i-blog.csdnimg.cn/blog_migrate/0cf0620a1f6b1bd3167dafb851c24548.png
 
图4.5 重定位节
4.3.5  符号表
符号表.symtab中存放了在程序中定义和引用的函数和全局变量的信息。
https://i-blog.csdnimg.cn/blog_migrate/040ce19e722470e72be74183637c3d3f.png
 
图4.6 符号表
4.4 Hello.o的结果解析

4.4.1  Objdump处理
https://i-blog.csdnimg.cn/blog_migrate/0a2ece3f88e25905e740ebef71692ff0.png
 
图4.7 objdump命令

https://i-blog.csdnimg.cn/blog_migrate/ad70bba97ea81644918544743969d6de.png
 
图4.8 objdump输出文件
4.4.2  分析hello.o的反汇编与hello.s对照分析
在立刻数的表示上,hello.s中的操作数表示为十进制,而hello.o反汇编代码中的数表示为十六进制。其原因是16进制更利于呆板解读和转换,而十进制更利于人类阅读。
在控制转移上,hello.s中使用.L2和.L3等段名称举行跳转,而反汇编代码使用目标代码的虚拟地点跳转。不外目前留下了重定位条目,跳转地点为零。在链接之后才会填写上正确的地点。
在函数调用上,hello.s直接call函数名称,而反汇编代码中call的是目标的虚拟地点。但和上一条的情况类似,只有在链接之后才气确定运行时正确的地点,目前目的地点是全0,并留下了重定位条目。
4.5 本章小结

本章先容了汇编内容。颠末汇编器,汇编语言转化为呆板语言,hello.s文件转化为hello.o可重定位目标文件。我们研究了可重定位目标文件elf格式,相识了readelf命令、elf头、节头部表、重定位节、符号表等内容。我们对比hello.s和hello.o,分析了汇编语言到呆板语言的变革。

第5章 链接

5.1 链接的概念与作用

现在,一开始的hello.c已经成为了hello.o,成为了二进制代码。但是如果hello.o不与需要调用的体系内核代码和数据、库的代码和数据等合为一体,程序仍然无法实行程序。
5.1.1什么是链接
链接是将各种差别文件(主要是可重定位目标文件)的代码和数据综合在一起,通过符号解析和重定位等过程,最终组合成一个可以在程序中加载和运行的单一的可实行目标文件的过程。
5.1.2为什么链接
链接令分离编译成为可能,方便了程序的修改和编译:无需重新编译整个工程,而是仅编译修改的文件。
链接另有利于构建共享库。源程序节流空间而未编入的常用函数文件(如printf.o)举行归并,生成可以正常工作的可实行文件。
5.2 在Ubuntu下链接的命令

https://i-blog.csdnimg.cn/blog_migrate/2978492a6ae500e4de5b2225d56d2b43.png
 
图5.1 ld链接命令
可以留意到使用ld命令时还链接了除了hello.o的其他的文件。
5.3 可实行目标文件hello的格式

5.3.1 readelf命令
https://i-blog.csdnimg.cn/blog_migrate/17866060aadaca196b65113ede58ff29.png
 
图5.2 readelf命令
运行后的结果定向输出到了elf__hello_o.txt中
5.3.2 ELF头
ELF头中包含了体系信息,编码方式,ELF头巨细,节的巨细和数目等等一系列信息。ELF头的具体内容如下:
https://i-blog.csdnimg.cn/blog_migrate/8492207950b0f23ff4454089a6452895.png
 
图5.3 ELF头
5.3.3  节头目表
节头目表描述了各个节的巨细、偏移量和其他属性。链接器链接时,会将各个文件的相同段归并成一个大段,并且根据这个大段的巨细以及偏移量重新设置各个符号的地点。
https://i-blog.csdnimg.cn/blog_migrate/105c4ca67da1b9a73afe233c3d088950.png
 
图5.4 节头目表
5.4 hello的虚拟地点空间

使用edb加载hello, data dump窗口可以查看加载到虚拟地点中的hello程序。program headers告诉链接器运行时加载的内容并提供动态链接需要的信息。
程序包含PHDR,INTERP,LOAD,DYNAMIC,NOTE,GNU_STACK,GNU_RELRO几个部分,此中PHDR 生存程序头表。INTERP 指定在程序已经从可实行文件映射到内存之后,必须调用的表明器。LOAD 表示一个需要从二进制文件映射到虚拟地点空间的段。此中生存了常量数据、程序的目标代码等。DYNAMIC 生存了由动态链接器使用的信息。NOTE 生存辅助信息。GNU_STACK:权限标志,用于标志栈是否是可实行。GNU_RELRO:指定在重定位结束之后哪些内存区域是需要设置只读。
https://i-blog.csdnimg.cn/blog_migrate/020d7cde9b705408182f8f39b51e863d.png
 
图5.5 程序结构
使用edb查看可以发现程序是从0x400000开始的,并且该处有ELF的标识,和ELF头中相匹配。
https://i-blog.csdnimg.cn/blog_migrate/61b84c8533b7a0d263ff5332d613b790.png
 
图5.6 edb使用Data Dump查看程序开始位置
.text起始位置为0x4010f0
https://i-blog.csdnimg.cn/blog_migrate/25506d99671c88f6a24575547a2a0067.png
 
图5.7 edb使用Data Dump查看.text起始位置
.rodata起始位置为0x4010f0
https://i-blog.csdnimg.cn/blog_migrate/fd392ea2d953382bff4691aeaf9384c0.png
 
图5.8 edb使用Data Dump查看.rodata起始位置
.PDHR起始位置为0x400040 巨细为0x2a0。
https://i-blog.csdnimg.cn/blog_migrate/13f3557e3814b81794a0d3d7e61ea28b.png
 
图5.9 edb使用Data Dump查看.PDHR起始位置
.INTERP起始位置为0x4002e0 巨细为0x1c。
https://i-blog.csdnimg.cn/blog_migrate/2540af31cd0fb60c7f6023b7159486a6.png
 
图5.10 edb使用Data Dump查看.INTERP起始位置
.DYNAMIC起始位置为0x403e50 巨细为0x1a0
https://i-blog.csdnimg.cn/blog_migrate/da4984ec22bcbdb9a8cd2df1c6f531ab.png
 
图5.11 edb使用Data Dump查看.DYNAMIC起始位置
.GNU_RELRO起始位置为0x403e50 巨细为0x1b0。
https://i-blog.csdnimg.cn/blog_migrate/f5836c4fd7bd866bb50db96048421e21.png
 
图5.12 edb使用Data Dump查看.GNU_RELRO起始位置
5.5 链接的重定位过程分析

5.5.1  Objdump处理
https://i-blog.csdnimg.cn/blog_migrate/e4e13642ab307ca86c067fe2a76c3b7b.png
 
图5.13 objdump命令
https://i-blog.csdnimg.cn/blog_migrate/93c8051e462ad8e4bff8b0a2ca412782.png
 
图5.14 objdump输出文件示例
5.5.2  新增函数
链接后在hello文件中参加了在hello.c中用到的库函数,如exit、printf、sleep、getchar等。
https://i-blog.csdnimg.cn/blog_migrate/706885ce84b77f2f0f7c6ed312da4807.png
 
图5.15 hello中新增的函数
5.5.3  新增节
在hello中增长了.init和.plt节,和一些节中定义的函数。
https://i-blog.csdnimg.cn/blog_migrate/a2df9505b689d960756b0fcc124fc8dc.png
 
图5.16 hello中新增的节示例
5.5.4  函数调用地点
重定位之后的hello在调用函数时调用的地点已经是函数确切的虚拟地点。
https://i-blog.csdnimg.cn/blog_migrate/fc33fda072a586a5c6149beb9a84d1e8.png
 
https://i-blog.csdnimg.cn/blog_migrate/46e9976d2faa2bbf67a6680011ff5ec4.png
 
图5.17 hello中已重定位的函数调用示例
5.5.5  控制流跳转地点
重定位之后的hello在跳转时调用的地点已经是函数确切的虚拟地点。
https://i-blog.csdnimg.cn/blog_migrate/48013c6a5d55517a4cdf4aa3b65b1f7c.png
 
https://i-blog.csdnimg.cn/blog_migrate/526fbfd49fb9eefce062dff2757d0875.png
 
图5.18 hello中已重定位的跳转地点示例
5.5.6  链接的过程
链接就是链接器(ld)将各个目标文件(各种.o文件)组装在一起,文件中的各个函数段按照肯定规则累积在一起。从.o提供的重定位条目将函数调用和控制流跳转的地点填写为最终的地点。
5.5.7  链接的过程分析
链接的过程主要分为符号解析和重定位这两个过程。
1)符号解析:符号解析解析目标文件定义和引用符号,并将每个符号引用和一个符号定义相关联。
2)重定位:编译器和汇编器生成从0开始的代码和数据节。而链接器通过把每个符号定义与一个虚拟内存地点相关联,从而将这些代码和数据节重定位,然后链接器会修改对所有这些符号的引用,使得它们指向这个虚拟内存地点。
对于hello来说,链接器把hello中的符号定义都与一个虚拟内存位置相关联,重定位了这些节,并在之后对符号的引用中把它们指向重定位后的地点。hello中每条指令都对应了一个虚拟地点,而且对每个函数,全局变量也都关联到了一个虚拟地点,在函数调用,全局变量的引用,以及跳转等操作时都通过虚拟地点来举行,从而实行这些指令。
5.6 hello的实行流程

从加载hello到_start,到call main,以及程序终止的所有过程如下:
_dl_start 地点:0x7f894f9badf0
_dl_init 地点:0x7f894f9cac10
_start 地点:0x4010f0
_libc_start_main 地点:0x7fce59403ab0
_cxa_atexit 地点:0x7f38b81b9430
_libc_csu_init 地点:0x4011c0
_setjmp 地点:0x7f38b81b4c10
_sigsetjmp 地点:0x7efd8eb79b70
_sigjmp_save 地点:0x7efd8eb79bd0
main 地点:0x401125
若argc != 4
puts 地点:0x401090
exit 地点:0x4010d0
此时输出窗口打印出“用法: Hello 学号 姓名 秒数!”,程序结束
若argc == 4
printf 地点:0x4010a0
atoi  地点:0x4010c0
sleep 地点:0x4010e0 (以上三个函数在循环体中实行5次)
此时窗口打印5行“Hello 2021112846 唐葳蕤”
getchar 地点:0x4010b0
等候用户输入回车,输入回车后:
_dl_runtime_resolve_xsave 地点:0x7f5852241680
_dl_fixup 地点:0x7f5852239df0
_uflow 地点:0x7f593a9a10d0
exit 地点:0x7f889f672120
程序终止。
5.7 Hello的动态链接分析

当程序调用一个共享库的函数时,编译器不能猜测这个函数在什么地点,因为定义它的共享模块在运行时可以加载到任何位置。这时,编译体系提供了耽误绑定的方法,即:将过程地点的加载推迟到第一次调用该过程时。通过观察edb对hello的实行情况,便可发现dl_init前后后.got.plt节发生的变革。
起首,通过readelf找到.got.plt节在地点为0x404000的地方开始,巨细为0x48。因此,结束地点为0x40400047,这两个地点之间部分便是.got.plt的内容。
https://i-blog.csdnimg.cn/blog_migrate/ad8323cee5984d04d8080bd2b0d9b4d4.png
 
图5.19 .got.plt节在虚拟内存中的位置
在edb中的Data Dump中找到这个地点,观察.got.plt节的,发现在dl_init前后,.got.plt的第9到22个字节发生了变革。
https://i-blog.csdnimg.cn/blog_migrate/e0175c0f313b654ab62bcbf34ff3bca1.png
 
图5.20 dl_init前.got.plt的状态
https://i-blog.csdnimg.cn/blog_migrate/dfee145cd3787e92b3e0764c21ccdee9.png
 
图5.21 dl_init后.got.plt的状态
在这里,这些变革的字节分别对应GOT和GOT的位置。此中, GOT包罗动态链接器在解析函数地点时使用的信息,而GOT则是动态链接器ld-linux.so模块中的入口点。加载时,动态链接器将重定位GOT中的这些条目,使它们包含正确的地点。内存的变革如上图所示。
5.8 本章小结

本章研究了链接的过程。通过edb查看hello的虚拟地点空间,对比hello与hello.o的反汇编代码,深入研究了链接的过程中重定位的过程。


第6章 hello进程管理

6.1 进程的概念与作用

现在hello已经成为了一个可以运行的程序了,运行它,它就会成为一个进程。
6.1.1什么是进程
进程(Process)是计算机中的程序关于某数据聚集上的一次运行活动,是体系举行资源分配和调理的根本单位,是操作体系结构的底子。在早期面向进程设计的计算机结构中,进程是程序的根本实行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
6.1.2为什么使用进程
进程提供给应用程序的关键抽象:一个独立的逻辑控制流,如同程序独占处理器;一个私有的地点空间,如同程序独占内存体系。可以说,如果没有进程,体系如此巨大的计算机不可能设计出来。
6.2 简述壳Shell-bash的作用与处理流程

6.2.1 壳Shell-bash的作用
Shell为用户提供命令行界面,使用户可以在这个界面中输入Shell命令,然后Shell实行一系列的读/求值步骤,在读步骤中读取用户的输入的命令行,在求值步骤中则解析命令行,并运行程序。不断重复上述步骤,直到用户退出Shell。从而完成用户与计算机的交互,以到达操作计算机的目的。
6.2.2 壳Shell-bash的处理流程
Shell打印一个命令行提示符,等候用户输入指令。在用户输入指令后,从终端读取该命令并举行解析,若该命令为Shell的内置命令,则立刻实行该命令;若不是内置命令,是一个可实行目标文件,则Shell创建会通过fork创建一个子进程,并通过execve加载并运行该可实行目标文件,用waitpid命令等候实行结束后对其举行接纳,从内核中将其删除;若将该文件转到背景运行,则shell返回到循环的顶部,等候下一个命令行。完成上述过程后,shell重复上述过程,直到用户退出shell。
6.3 Hello的fork进程创建过程

https://i-blog.csdnimg.cn/blog_migrate/958757647e1885c43915e675ed7f698f.png
 
图6.1 fork()创建进程的具体步骤
6.4 Hello的execve过程

execve函数的参数包罗需要实行的程序(通常是argv)、参数argv、情况变量envp。 execve的具体操作步骤如下:
1. 删除已存在的用户区域(从父进程独立)。
2. 映射私有区:为Hello的代码、数据、.bss和栈区域创建新的区域结构,所有这些区域都是私有的、写时才复制的。
3. 映射共享区:比如Hello程序与标准C库libc.so链接,这些对象都是动态链接到Hello的,然后存放在用户虚拟地点空间中的共享区域内。
4. 设置PC:exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。
5. execve在调用乐成的情况下不会返回,只有当出现错误时,例如找不到需要实行的程序时,execve才会返回到调用程序。
6.5 Hello的进程实行

6.5.1 逻辑控制流和时间片
进程的运行本质上是CPU不断从程序计数器 PC 指示的地点处取出指令并实行,值的序列叫做逻辑控制流。操作体系会对进程的运行举行调理,实行进程A->上下文切换->实行进程B->上下文切换->实行进程A->… 如此循环往复。 在进程实行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调理,是由内核中称为调理器的代码处理的。当内核选择一个新的进程运行,我们说内核调理了这个进程。在内核调理了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。在一个程序被调运行开始到被另一个进程打断,中间的时间就是运行的时间片。
6.5.2 用户模式和内核模式
用户模式的进程不允许实行特殊指令,不允许直接引用地点空间中内核区的代码和数据。
内核模式进程可以实行指令集中的任何命令,并且可以访问体系中的任何内存位置。
6.5.3 上下文
上下文就是内核重新启动一个被抢占的进程所需要恢复的原来的状态,由寄存器、程序计数器、用户栈、内核栈和内核数据结构等对象的值构成。
6.5.4 调理的过程
在对进程举行调理的过程,操作体系主要做了两件事:加载生存的寄存器,切换虚拟地点空间。
6.5.5 用户态与核心态转换
为了能让处理器安全运行,需要限定应用程序可实行指令所能访问的地点范围。因此划分了用户态与核心态。
核心态可以说是拥有最高的访问权限,处理器以一个寄存器当做模式位来描述当前进程的特权。进程只有故障、中断或陷入体系调用时才会得到内核访问权限,其他情况下始终处于用户权限之中,保证了体系的安全性。
6.6 hello的非常与信号处理

6.6.1 正常运行状态
https://i-blog.csdnimg.cn/blog_migrate/03796c87a02ce1b6fa35d27d3976582b.png
 
图6.2 hello正常运行状态
6.6.2 非常范例与处理方式
类别
原因
异步/同步
返回活动
中断
来自I/O装备的信号
异步
总是返回到下一条指令
陷阱
故意的非常
同步
总是返回到下一条指令
故障
潜在可恢复的错误
同步
可能返回到当前命令
终止
不可恢复的错误
同步
不会返回
https://i-blog.csdnimg.cn/blog_migrate/930ac761d91ee506d581fc61021209e0.png
 
图6.3 几种非常范例与处理方式
6.6.3 发送信号
1. 按下Ctrl+Z:
进程收到 SIGSTP 信号,hello 进程挂起。用ps查看其进程PID,可以发现hello的PID是2111;再用jobs查看此时hello的背景 job号是1,只需调用 fg 1即可将其调回前台继续实行。
https://i-blog.csdnimg.cn/blog_migrate/748f138bc2495356830d3e0a7e28beca.png
 
图6.4 Ctrl+Z挂起进程
2. 按下Ctrl+C:
进程收到 SIGINT 信号,使得hello程序终止。在ps中查询不到其PID,输入jobs也没有体现,可以看出hello已经彻底结束。
https://i-blog.csdnimg.cn/blog_migrate/38f1f93f542565709063d31da04c4b38.png
 
图6.5 Ctrl+C结束进程
3.中途乱按:
输入的字符只会缓存到缓冲区,但由于没有发送有效的信号,程序本身的运行不会被干扰。如果乱按的过程中没有输入Enter回车,那么由于getchar的特性,还需要按一个回车才气退出程序。
https://i-blog.csdnimg.cn/blog_migrate/1b514569f874b395d7e967ab060ef4c0.png
 
图6.6 实行中途乱按的效果(没按回车键)
4.不断按下回车:
效果和乱按相同,输入的字符只会缓存到缓冲区,不会干扰程序运行。但差别的是由于按下过回车,在getchar函数处会直接退出,缓冲区的回车会反映在命令行中。
https://i-blog.csdnimg.cn/blog_migrate/b28fcc9be8e8302c01aa8d59e21c0b76.png
 
图6.7 实行中不断按回车的效果
5.Kill命令:
在运行途中对进程使用kill命令,进程将被终止,在ps中无法查到到其PID,结果和Ctrl+C终止一样。
https://i-blog.csdnimg.cn/blog_migrate/2fae35f1f5e8942acacb088dab820933.png
 
图6.8 使用kill命令杀死实行中的hello程序
6.7本章小结

本章相识了hello进程的实行过程。在hello运行过程中,内查对其调理,非常处理程序为其将处理各种非常。每种信号都有差别的处理机制,对差别的shell命令,hello也有差别的相应结果。

第7章 hello的存储管理

7.1 hello的存储器地点空间

7.1.1 逻辑地点
逻辑地点(Logical Address)是指由程序产生的与段相关的偏移地点部分。在这里指的是hello.o中的内容。
7.1.2 线性地点
线性地点(Linear Address)是逻辑地点到物理地点变换之间的中间层。程序hello的代码会产生段中的偏移地点,加上相应段的基地点就生成了一个线性地点。
7.1.3 虚拟地点
CPU启动保护模式后,程序hello运行在虚拟地点空间中。留意,并不是所有的“程序”都是运行在虚拟地点中。CPU在启动的时间是运行在实模式的,Bootloader以及内核在初始化页表之前并不使用虚拟地点,而是直接使用物理地点的。
7.1.4 物理地点
计算机体系的主存被组织成一个由 M 个连续的字节巨细的单元组成的数组,此中每一个字节都被给予一个唯一的物理地点。如果是读取,电路根据这个地点每位的值就将相应地点的物理内存中的数据放到数据总线中传输。如果是写入,电路根据这个地点每位的值就在相应地点的物理内存中放入数据总线上的内容。物理内存是以字节(8位)为单位编址的。在hello的运行过程中,hello内的虚拟地点颠末地点翻译后得到的即为物理地点,并在通过物理地点来访问数据。
7.2 Intel逻辑地点到线性地点的变换-段式管理

在 Intel 平台下,逻辑地点(logical address)是 selector:offset 这种形式,selector 是 CS 寄存器的值,offset 是 EIP 寄存器的值。如果用 selector 去 GDT(全局描述符表)里拿到 segment base address(段基址)然后加上 offset(段内偏移),这就得到了 linear address。我们把这个过程称作段式内存管理。
一个逻辑地点由段标识符和段内偏移量组成。段标识符是一个16位长的字段(段选择符)。可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。
全局的段描述符,放在“全局段描述符表(GDT)”中,一些局部的段描述符,放在“局部段描述符表(LDT)”中。
给定一个完备的逻辑地点段选择符+段内偏移地点,看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地点和巨细。拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,就得到了其基地点。base + offset =线性地点。
7.3 Hello的线性地点到物理地点的变换-页式管理

页式管理是一种内存空间存储管理的技能,页式管理分为静态页式管理和动态页式管理。将各进程的虚拟空间划分成若干个长度相称的页(page),页式管理把内存空间按页的巨细划分成片大概页面(page frame),然后把页式虚拟地点与内存地点建立一一对应页表,并用相应的硬件地点变换机构,来解决离散地点变换问题。页式管理接纳请求调页或预调页技能实现了内外存存储器的统一管理。
https://i-blog.csdnimg.cn/blog_migrate/c7198c95c2a582b3ddcbb2fc77950750.png
 
图7.1 页式管理例子
7.4 TLB与四级页表支持下的VA到PA的变换

7.4.1 翻译后备缓冲器
每次CPU产生一个虚拟地点,MMU(内存管理单元)就必须查阅一个PTE(页表条目),以便将虚拟地点翻译为物理地点。在最糟糕的情况下,这会从内存多取一次数据,代价是几十到几百个周期。如果PTE碰巧缓存在L1中,那么开销就会降落1或2个周期。然而,很多体系都试图消除即使是如许的开销,它们在MMU中包罗了一个关于PTE的小的缓存,称为翻译后备缓存器(TLB)。
7.4.2 多级页表
将虚拟地点的VPN划分为相称巨细的差别的部分,每个部分用于探求由上一级确定的页表基址对应的页表条目。
https://i-blog.csdnimg.cn/blog_migrate/493bc640753ea6e56b4dfb28df5728c6.png
 

图7.2 课本中k级页表的地点翻译示例图

7.4.3 VA到PA的变换
处理器生成一个虚拟地点,并将其传送给MMU。MMU用VPN向TLB请求对应的PTE,如果命中,则跳过之后的几步。MMU生成PTE地点(PTEA).,并从高速缓存/主存请求得到PTE。如果请求不乐成,MMU向主存请求PTE,高速缓存/主存向MMU返回PTE。PTE的有效位为零, 因此 MMU触发缺页非常,缺页处理程序确定物理内存中要换出的页 (若页面被修改,则换出到磁盘——写回策略)。缺页处理程序调入新的页面,并更新内存中的PTE。缺页处理程序返回到原来进程,再次实行导致缺页的指令。
在多级页表的情况下,无非是不断通过索引-地点-索引-地点重复举行查找。
7.5 三级Cache支持下的物理内存访问

CPU发送一条虚拟地点,随后MMU按照7.4所述的操作获得了物理地点PA。根据cache巨细组数的要求,将PA分为CT(标记位)CI(组索引),CO(块偏移)。根据CI探求到正确的组,依次与每一行的数据比力,有效位有效且标记位一致则命中。如果命中,直接返追念要的数据。如果不命中,就依次去L2,L3,主存判断是否命中,命中时将数据传给CPU同时更新各级cache的储存。
https://i-blog.csdnimg.cn/blog_migrate/e2137fb41581995f106735f9272e471e.png
 
图7.3 三级Cache支持下的物理内存访问
7.6 hello进程fork时的内存映射

当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的PID,同时为这个新进程创建虚拟内存,创建当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记位只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中的任一个厥后举行写操作时,写时复制机制就会创建新页面。
7.7 hello进程execve时的内存映射

1)在bash中的进程中实行了如下的execve调用: execve("hello",NULL,NULL);
2)execve函数在当前进程中加载并运行包含在可实行文件hello中的程序,用hello替换了当前bash中的程序。
3)删除已存在的用户区域。
4)映射私有区域
5)映射共享区域
6)设置程序计数器(PC)
7)最后,exceve设置当前进程的上下文中的程序计数器到代码区域的入口点。
7.8 缺页故障与缺页中断处理

页面命中完全是由硬件完成的,而处理缺页是由硬件和操作体系内核协作完成的:
https://i-blog.csdnimg.cn/blog_migrate/4ea8973a0c38b34d97289763b65b6635.png
 
图7.4 缺页处理表示图
1)处理器生成一个虚拟地点,并将它传送给MMU
2)MMU生成PTE地点,并从高速缓存/主存请求得到它
3)高速缓存/主存向MMU返回PTE
4)PTE中的有效位是0,以是MMU出发了一次非常,传递CPU中的控制到操作体系内核中的缺页非常处理程序。
5)缺页处理程序确认出物理内存中的牺牲页,如果这个页已经被修改了,则把它换到磁盘。
6)缺页处理程序页面调入新的页面,并更新内存中的PTE
7)缺页处理程序返回到原来的进程,再次实行导致缺页的命令。CPU将引起缺页的虚拟地点重新发送给MMU。因为虚拟页面已经换存在物理内存中,以是就会命中。
7.9动态存储分配管理

7.9.1 动态内存管理的根本方法
当C程序运行时需要额外虚拟内存时,用动态内存分配器是一种更加方便、有更好可移植性的方法。
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向更高的地点延伸。对于每个进程,内核维护着一个变量brk,它指向堆的顶部。
分配器将内存视为一组差别巨细的块的聚集来维护,每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块保存供应用程序使用,空闲块可用来分配。
分配器有两种根本风格:显式分配器与隐式分配器。此中显式分配器要求应用显式地释放任何已分配的块,如C语言中调用malloc函数来分配一个块,然后调用free函数来释放一个块。隐式分配器要求分配器检测一个已分配块何时不再被程序所使用,就释放这个块。因此,隐式分配器也叫垃圾分配器。
在hello中,printf函数会调用malloc函数。malloc函数返回一个指针,指向巨细至少为size字节的内存块。
当程序不再需要malloc分配的区域时,需要通过向free函数传递不再需要的区域的指针举行释放。
7.9.2 动态内存管理的策略
程序使用动态内存分配最重要的原因是:有时,只有在我们运行程序时,才气知到某些数据结构的巨细,如许便需要使用到动态内存分配。
例如,要求一个C语言程序读一个ASCII码整数的链表,每一行一个整数,从stdin到一个C数组。输入是由整数n和接下来要读和存储到数组中的n个整数组成的。最简单的方法就是静态地定义这个数组,最大数组巨细MAXN固定。
然而,如果这个程序使用者想要读取一个比MAXN大的文件,唯一的办法就是修改程序中MAXN的值,对大型软件产品而言不是一个好方法。
一种更好的方法是在已知n的值后,动态地分配这个数组。使用这种方法,数组巨细的最大值就只由虚拟内存数目来限定了。
hello中的printf作为一个已经编译、汇编好了,等候链接的函数,修改固定参数也是不现实的。
起首,对堆中的块举行组织,可以选择隐式、体现、分离空闲链表等。当分配器查找空闲块时,又可以接纳差别的放置策略,如初次适配(从头开始搜索链表)、下一次适配(从上一次找到的空闲块的剩余块除法)以及最佳适配等。在分割空闲块时,又可以接纳将剩余块分割为新的空闲块的策略。在归并时,可以接纳带边界标记的归并,就像在上一段根本方法中所描述,通过边界标记来判断当前块四周是否也同样是空闲块,以此来判断是否需要归并。
7.10本章小结

本章主要先容了hello的存储器地点空间。联合了hello,说明白逻辑地点、线性地点、虚拟地点、物理地点的概念,以及它们的区别与接洽,互相转化的方法。
分析了段式管理是如何完成逻辑地点到线性地点(虚拟地点)的变换的,包罗段选择符和段内偏移量的作用。
分析了页式管理是如何完成线性地点到物理地点的变换的。
分析了TLB与四级页表支持下的VA到PA的变换。高速地点变址缓存TLB有加速对于页表访问的功能。以四级页表为例,先容了多级页表的条理、工作流程以及节流空间的优点。
先容了三级Cache支持下的物理内存访问的流程,包罗在命中情况下的与未命中情况下的。
分析了hello进程fork与execve时的内存映射,偏重从写时复制机制先容了创建子进程时的虚拟内存相同且独立与物理内存共享。
先容了缺页故障与缺页中断的处理。以一个VM缺页为例,先容了缺页中断的处理流程。
分析了动态存储分配管理。从动态内存管理的根本方法与动态内存管理的策略两个方面对动态内存管理举行先容。

第8章 hello的IO管理

8.1 Linux的IO装备管理方法

在Linux中,所有的I/O装备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来实行。
这种将装备映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这就是Unix I/O接口。
这使得所有的输入和输出都能以一种统一且一致的方式来实行。这便是Linux的IO装备管理方法。
8.2 简述Unix IO接口及其函数

8.2.1 open()打开文件
头文件:
#include <sys/types.h>
#include <sys/stat.h>
函数信息:
int open(const char *pathname, int flags);
参数1: pathname,文件所在路径。
参数2: flags,文件权限,相对于程序进程。
常见宏有:O_WRONLY, O_RDONLY, O_RDWR, O_EXCL, O_APPEND, O_DUMP等。
参数3: mode,当创建文件时间使用,一般为umask的值。
返回值:乐成返回文件描述符,否则返回-1。
8.2.2 close()关闭文件
函数信息:
int open(int fd);
参数1: fd,文件描述符。
返回值:乐成返回0,否则返回-1。
8.2.3 write()向文件中写入数据
函数信息:
ssize_t write(int fd, const void *buf, size_t count);
参数1: fd,文件描述符。
参数2: buf:要写入的数据。
参数3: count,写入的数据的长度。
返回值:乐成返回写入的长度,否则返回-1。
8.2.4 read()读入文件中的数据
函数信息:
ssize_t read(int fd, const void *buf, size_t count);
参数1: fd,文件描述符。
参数2: buf:要读入的数据存放内存指针。
参数3: count,读入的数据的长度。
返回值:乐成返回读入的长度,否则返回-1。
8.2.5 lseek()修改文件偏移量
头文件:
#include <sys/types.h>
#include <unistd.h>
函数信息:
off_t lseek(int fd,off_t offset, int whence);
参数1: fd,文件描述符。
参数2: offset:将要偏移的字节数。
参数3: whence:从那开始偏移,宏定义如下:SEEK_END文件末尾,SEEK_CUR当前偏移量位置,SEEK_SET文件开头位置。
留意当偏移量大于文件长度时,会产生空洞,空洞是由所设置的偏移量凌驾文件尾端,并写了一些数据造成了,其从原文件尾端到新偏移的位置之间便是产生的空洞。空洞固然有数据但是不占用磁盘空间。
8.2.6 access()判断文件是否具有读写可实行权限大概是否存在
函数信息:
int access(const char *pathname,int mode) ;
参数1: pathname,文件名。
参数2:mode可以选择以下宏:F_OK文件是否存在R_OK文件否具有读权限x_OK文件否具有可实行权限w_OK文件否具有写权限。
返回值:返回值:满足mode中的参数并且正确实行则返回0,否则返回-1。
8.2.7 dup()或dup2()创建一个描述符,其指向同一个文件表
函数信息:
int dup(int oldfd) ;
参数1: oldfd,原来的文件描述符。
在此简单先容一下文件的内核结构:起首计算机体系中有一个进程表,此中的每个进程表项,该表项中有一个打开文件描述符表,该打开的文件描述表中有很多文件描述符表项,每项包罗两部分:文件描述符标志与文件指针,此中文件指针指向一个文件表,文件表中存放着文件的状态标志便是否可读是否可写,当前文件的偏移量,另有一个v节点指针,指针v节点指针指向一个v节点表,v节点表主要存放文件的所有者,文件长度,文件的装备以及文件现实数据块在磁盘上的位置等一系列信息。可能如许描不太清楚,下面用一张图来描述:
https://i-blog.csdnimg.cn/blog_migrate/542d99aa75e347209c21ef7e248ce21f.png
 
图8.1 文件的内核结构表示图
8.3 printf的实现分析

https://i-blog.csdnimg.cn/blog_migrate/f23807d5df5cabb3cde4831439a03f4a.png
 
图8.2 printf函数
可以发现printf的第一个参数是const char*范例的形参fmt,而后面的参数用…取代,是一种可变形参的写法,因为对于printf来说传入的参数个数不确定。那么为了正确举行打印,在之后便需要设法得知传入参数的个数。
va_list arg = (va_list)((char*)(&fmt) + 4);
这句说明它是一个字符指针,此中的(char*)(&fmt) + 4) 表示的是...中的第一个参数。这是因为C语言中,参数压栈的方向是从右往左的。也就是说,当调用printf函数的适合,先是最右边的参数入栈。
接下来函数调用了vsprintf函数。
https://i-blog.csdnimg.cn/blog_migrate/c68f53a0034ee7376f0c2740220924cf.png
 
图8.3 printf引用的vsprintf函数
起首阅读一下vsprintf(buf, fmt, args)函数。颠末分析发现,vsprintf返回的是要打印的字符串的长度。
而后面一句:write(buf, i)是写操作,就是传入buf与参数数目i,将buf中的i个元素写到终端。那么再阅读write的汇编代码:
write: 
    mov eax, _NR_write 
    mov ebx,  
    mov ecx,  
    int  INT_VECTOR_SYS_CALL 
就是给寄存器传递了参数,然后int INT_VECTOR_SYS_CALL结束。再找到INT_VECTOR_SYS_CALL的实现:
init_idt_desc(INT_VECTOR_SYS_CALL,DA_386IGate, sys_call, PRIVILEGE_USER);
int INT_VECTOR_SYS_CALL表示要通过体系来调用sys_call这个函数。
最后看到sys_call的汇编代码:
sys_call: 
    call save 
    push dword  
    sti 
    push ecx 
    push ebx 
    call  
    add esp, 4 * 3 
    mov , eax 
    cli 
    ret 
在这里,sys_call实现了体现格式化了的字符串,也就是ASCII到字模库到体现vram的信息。从而实现了字符串的体现。
8.4 getchar的实现分析

#define getchar() getc(stdin)
以上就是getchar()的实现,可以发现getchar()其实是一个宏函数。
getchar()有一个int型的返回值。当程序调用getchar时,程序就等着用户按键。用户输入的字符被存放在键盘缓冲区中,直到用户按回车为止(回车字符也放在缓冲区中)。当用户键入回车之后,getchar才开始从stdin流中每次读入一个字符。
getchar函数的返回值是用户输入的第一个字符的ASCII码,如堕落则返回-1,且将用户输入的字符回显到屏幕。
如用户在按回车之前输入了不止一个字符,其他字符会保存在键盘缓存区中,等候后续getchar调用读取。也就是说,后续的getchar调用不会等候用户按键,而是直接读取缓冲区中的字符,直到缓冲区中的字符读完后,才继续等候用户按键。
这可以看做一个异步非常-键盘中断的处理:键盘中断处理子程序。担当按键扫描码转成ascii码,生存到体系的键盘缓冲区。getchar函数调用read体系函数,通过体系调用读取按键ascii码,直到担当到回车键才返回。
8.5本章小结

本章主要先容了hello的IO管理,从Linux的IO装备管理方法入手,先容了Linux中,所有的I/O装备都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来实行。分析了Unix I/O接口及其打开文件、关闭文件、读写文件时的功能以及相应的函数使用。分析了printf()函数是如安在体现屏上打印字符串的,主要运用到了Unix I/O接口写的思想。分析了getchar()函数是如何从缓冲区读入键盘的输入的,主要运用到了Unix I/O接口读的思想,还包罗异步非常——键盘中断的处理。


结论

hello程序的生命周期是从一个高级C语言程序开始的。
为了在体系上运行hello.c程序,hello.c起首颠末了预处理器(cpp)得到修改了的源程序hello.i。这时的hello.i代码量比hello.c大大增长,此中注释部分被删除了,引用的头文件被插入到了源代码中,除头文件与注释以外的源代码保持稳定。
接着,编译器(cc1)将hello.i翻译为汇编程序hello.s。在这个过程中,高级语言被翻译为呆板逻辑下的汇编语言,编译器可能会根据编译选项的差别对程序举行一些优化。
然后hello.s颠末汇编器(as)翻译成呆板语言指令,把这些指令打包成可重定位目标程序hello.o。hello.o是一个二进制文件,可以查看它的ELF格式和相应的反汇编代码。查看ELF格式可以发现,hello.o是由差别的节组成的,每个节都有相应的巨细和功能。
接下来,颠末链接器,将调用的标准C库中的函数(如printf等)对应的预编译好了的目标文件以某种方式归并到hello.o文件中,得到可实行目标程序hello。hello也是一个二进制文件,可以查看它的ELF格式与反汇编代码。相比于hello.o,hello的代码量大大增长,这是由于一些C标准库中的函数(如printf)等被归并到了hello中。同时,每一行代码、字符串常量等,都会被分配相应的虚拟地点。这是的hello是可以直接运行的文件。
当我们运行hello时,在shell中利用fork()函数创建子进程,为子进程分配一个与父进程相同但独立的虚拟内存空间,实行写时复制机制,再用execve()函数加载hello程序,这时,hello就由程序(program)变成了一个进程(process)。
映射虚拟内存,程序开始时载入物理内存,进入CPU处理。CPU为实行文件hello分配时间片,举行取指、译码、实行等流水线操作。
内存管理器和CPU在实行过程中通过L1、L2、L3三级缓存和TLB多级页表,颠末一系列地点变换,从逻辑地点,到线性地点,最终得到物理地点,利用物理地点,从内存中取得数据。
最后通过I\O体系根据代码指令举行输出。在程序运行结束后,父进程会对其举行接纳,内核把它从体系中清除,最后不留下一点陈迹。这就是hello所经历的一生的过程。


附件

文件作用
文件名
C语言源文件
hello.c
预处理后的文件
hello.i
编译之后的汇编文件
hello.s
汇编之后的可重定位目标文件
hello.o
链接之后的可实行目标文件
hello
hello.o的ELF格式
elf__hello_o.txt
hello.o的反汇编代码
dump__hello_o.txt
hello的ELF格式
elf__hello.txt
hello的反汇编代码
dump__hello.txt
截取的printf和vsprintf函数实现代码
printf&vsprintf.c




















参考文献

  https://blog.csdn.net/lll_90/article/details/85427841
  https://zhuanlan.zhihu.com/p/384701815
  https://zhuanlan.zhihu.com/p/517482086
  深入明白计算机体系(原书第3版)/(美)兰德尔·E.布莱恩特(Randal E. Bryant)等著;龚奕利,贺莲.—北京:机械工业出书社,2016.7(2019.3重印)

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 计算机体系大作业程序人生-Hello’s P2P-2023