计算机系统

大作业

题 目:程序人生-Hello’s P2P

专 业:计算机

学 号:1190201224

班 级:1903005

学 生:徐亚楠

指 导 教师:史先俊

计算机科学与技术学院

2021年5月

Hello 的自白:

  • 俺是Hello,额是每一个程序猿¤的初恋(羞羞……)

  • 却在短短几分钟后惨遭每个菜鸟的无情抛弃(呜呜……),他们很快喜欢上sum、sort、matrix、PR、AI、IOT、BD、MIS……,从不回头。

  • 只有我自己知道,我的出身有多么高贵,我的一生多么坎坷!

  • 多年以后,那些真正懂我的大佬(也是曾经的菜鸟一枚),才恍然感悟我的伟大!

  • ……………………想当年:俺才是第一个玩 P2P的: From Program to Process

  • 懵懵懂懂的你笨笨磕磕的将我一字一键敲进电脑存成hello.c(Program),无意识中将我预处理、编译、汇编、链接,历经艰辛-神秘-高贵-欣喜,我-Hello一个完美的生命诞生了。

  • 你造吗?在壳(Bash)里,伟大的OS(进程管理)为我fork(Process),为我execve,为我mmap,分我时间片,让我得以在Hardware(CPU/RAM/IO)上驰骋(取指译码执行/流水线等);

  • 你造吗?OS(存储管理)与MMU为VA到PA操碎了心;TLB、4级页表、3级Cache,Pagefile等等各显神通为我加速;IO管理与信号处理使尽了浑身解数,软硬结合,才使我能在键盘、主板、显卡、屏幕间游刃有余,
    虽然我在台上的表演只是一瞬间、演技看起来很Low、效果很惨白。

  • 感谢 OS!感谢 Bash!在我完美谢幕后为我收尸。 我赤条条来去无牵挂!

  • 我朝 CS(计算机系统-Editor+Cpp+Compiler+AS+LD + OS +
    CPU/RAM/IO等)挥一挥手,不带走一片云彩!

  • 历史长河中一个个菜鸟与我擦肩而过,只有CS知道我的生、我的死,我的坎坷,“只有
    CS 知道……我曾经……来…………过……”

摘 要

本文通过模拟计算机系统在运行时对hello这一最基本的程序实例的操作对本学期所学计算机系统所学知识进行了一个串联应用。本文借助Linux下系列开发工具,通过对hello.c程序进行模拟计算机进行预处理、编译、汇编等过程的来窥探程序在操作系统例如Linux中的实现机制及原因。同时通过对hello在Shell中的动态链接、进程运行、内存管理、I/O管理等过程的探索来更深层次的理解Linux系统下的动态链接机制、存储层次结构、异常控制流、虚拟内存及UnixI/O等相关内容。

关键词:操作系统:动态链接:进程运行:内存管理:I/O管理:异常控制流:虚拟内存

**
**

目 录

第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:From Program to Process

gcc编译器驱动程序读取程序文件hello.c,然后由预处理器(cpp)根据以#字符开头的命令,修改该程序获得hello.i文件,通过编译器(cll)将文本文件hello.i翻译成文本文件形式的汇编程序hello.s,再通过汇编器(cs)将hello.s翻译成机器语言指令,将指令打包成可重定位的目标文件hello.o,然后通过链接器(ld)合并获得可执行目标文件hello。Linux系统中通过内置命令行解释器shell加载运行hello程序,为hello程序fork进程,至此,hello.c完成了P2P的过程。

020:From Zero-0 to Zero-0

Shell通过execve在fork产生的子进程中加载hello,先删除当前虚拟地址的用户部分已存在的数据结构,为hello的代码、数据、bss和栈区域创建新的区域结构,然后映射共享区域,设置程序计数器,使之指向代码区域的入口点,进入main函数,CPU为hello分配时间片执行逻辑控制流。hello通过Unix I/O管理来控制输出。hello执行完成后shell会回收hello进程,并且内核会从系统中删除hello所有痕迹,至此,hello完成020的过程。

1.2 环境与工具


操作系统:Windows10 64位; Ubuntu 20.10 LTS 64
虚拟机:VMware 15.5 2
开发工具:CodeBlocks;vi/vim/gedit+gcc;Visual Studio2019 64位;GDB/OBJDUMP;EDB等

1.3 中间结果

1.hello.c:源代码
2.hello.i:hello.c预处理生成的文本文件
3. hello.s:hello.i经过编译器翻译成的文本文件hello.s,含汇编语言程序
4. hello.o:hello.s经汇编器翻译成机器语言指令打包成的可重定位目标文件
5. hello.elf:hello.o的ELF格式
6. hello_o_asm.txt:hello.o反汇编生成的代码。
7. hello:经过hello.o链接生成的可执行目标文件。
8.hello_out.elf:hello的ELF格式。
9. hello_out_asm.txt:hello反汇编生成的代码。

1.4 本章小结

本章简要介绍了此次大作业所需的实验环境,以及编译、链接等过程产生的对应文件。简要概述了hello人生的背景——针对hello程序的探究,漫游式地了解hello在系统中的生命周期。简述了探究的流程:P2P020

第2章 预处理

2.1 预处理的概念与作用

概念:预处理是C语言程序从源代码变成可执行程序的第一步,是在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。由预处理器(preprocessor)
对程序源代码进行处理,再由编译器进一步编译。

作用:预处理把源代码分割或处理成为特定的单位——预处理记号(preprocessing
token)用来支持语言特性(如C/C++的宏调用)。

预处理指令是以#号开头的代码行。#号必须是该行除了任何空白字符外的第一个字符。#后是指令关键字,在关键字和#号之间允许存在任意个数的空白字符。整行语句构成了一条预处理指令,该指令将在编译器进行编译之前对源代码做某些转换。下图是ANSI标准定义的C语言预处理指令:

指令 意义
# 空指令,无任何效果
#define 定义宏
#undef 取消已定义的宏
#if 如果给定条件为真,则编译下面代码
#ifdef 如果宏已经定义,则编译下面代码
#ifndef 如果宏没有定义,则编译下面代码
#elif 如果前#if条件不为真,当前条件为真,则编译下面代码,其实就是else if的简写
#endif 结束一个#if……#else条件编译块
#error 停止编译并显示错误信息

主要功能:

文件包含。例如hello.c中第一行的#include<stdio.h>命令告诉预处理器读取系统头文件stdio.h文件的内容,并把它直接插入到程序文本中;

2. 删除注释;

3. 执行宏替代。宏是对一段重复文字的简短描写,例如#define MAX
2147483647在预处理中会把所有MAX替代为2147483647,#define MAX(x,y)
((x)>(y))?(x): (y)在预处理中会把所有MAX(x,y)替换为((x)>(y))?(x): (y);

条件编译。是根据实际定义宏(某类条件)进行代码静态编译的手段。可根据表达式的值或某个特定宏是否被定义来确定编译条件。

2.2在Ubuntu下预处理的命令

在Ubuntu系统终端中输入命令:

gcc -m64 -no-pie -fno-PIC -E -o hello.i hello.c

将hello.c预处理生成hello.i

2.3 Hello的预处理结果解析

  1. 在预处理文本文件hello.i中,首先是对文件包含中系统头文件的寻址和解析,如下:

  2. hello.i内容:
    因为hello.c包含的头文件中还包含有其他头文件,因此系统会递归式的寻址和展开,直到文件中不含宏定义且相关的头文件均已被引入。同时引入了头文件中所有typedef关键字,结构体类型、枚举类型、通过extern关键字调用并声明外部的结构体及函数定义,例如下图:

3.在最后引入main函数,如下:

2.4 本章小结

本章介绍了预处理的概念、作用以及在Ubuntu下通过gcc指令进行预处理的方法,通过对hello.c进行预处理,将hello.c与hello.i进行对比说明了预处理阶段做了怎样的处理。

第3章 编译

3.1 编译的概念与作用

概念:
编译是利用编译程序从预处理文本文件产生汇编程序(文本)的过程。主要包含五个阶段:词法分析;语法分析;语义检查、中间代码生成、目标代码生成。

作用:编译作用主要是将文本文件hello.i翻译成文本文件hello.s,并在出现语法错误时给出提示信息,执行过程主要从其中三个阶段进行分析:

  1. 词法分析。词法分析的任务是对由字符组成的单词进行处理,从左至右逐个字符地对源程序进行扫描,产生一个个的单词符号,把作为字符串的源程序改造成为单词符号串的中间程序;

  2. 语法分析。语法分析器以单词符号作为输入,分析单词符号串是否形成符合语法规则的语法单位,如表达式、赋值、循环等,最后看是否构成一个符合要求的程序,按该语言使用的语法规则分析检查每条语句是否有正确的逻辑结构,程序是最终的一个语法单位;

  3. 目标代码生成。目标代码生成器把语法分析后或优化后的中间代码经汇编程序汇编生成汇编语言代码,成为可执行的机器语言代码。

3.2 在Ubuntu下编译的命令

在Ubuntu系统终端中输入命令:

gcc -m64 -no-pie -fno-PIC -S -o hello.s hello.i

3.3 Hello的编译结果解析

3.3.1汇编文件伪指令:
指令 对应内容
.file 声明源文件
.text 声明代码段
.data 声明数据段
.align 声明指令及数据存放地址的对齐方式
.type 指定类型
.size 声明大小
.section .rodata 只读数据
.globl 全局变量
3.3.2数据类型:
  1. 字符串型:

汇编语言中,输出字符串作为全局变量保存,因此存储于.rodata节中。汇编文件hello.s中,共有两个字符串,均作为printf参数,分别为:

输出字符串1:

原本字符串为“Usage: Hello 学号
姓名!”,对应中文字符可以看出对中文字符进行了utf-8编码,中文汉字以‘\345’开头,占三个字符,而全角字符‘!’占用两个字符。

输出字符串2:

该字符串为printf格式化输出的字符串,正常保存。

  1. 整型:

针对汇编文件中出现的整型数:

全局变量sleepsecs

在hello.c文件中,sleepsecs的定义如下:

sleepsecs被定义为整型全局变量,因此对2.5进行向下取整,sleepsecs为2,根据对齐方式.align为4,sleepsecs的size大小定义为了4Bytes。hello.c中定义sleepsecs为整型,但是在hello.s中可以看出其类型为.long。

编译器ccl会将int表示为long但对齐方式仍为int型的4字节,long类型表示为双字quad,对齐方式仍为8字节。

3. 数组:

在hello.c中有对数组的应用,如下:

hello.s循环输出部分:

argv[2]作为printf函数的第三个参数,应当存于寄存器%rdx中,因此可推断argv[2]地址为-0x16(%rbp);argv[1]作为printf第二个参数,应当存于寄存器%rsi中,因此可推断argv[1]地址为-0x2a(%rbp)中,数组首地址位于-0x32(%rbp),以上所占字节数为8。

汇编语言操作:
  1. 数据传送指令:
  • MOV类

MOV类主要由4条指令组成:movb、movw、movl、movq。主要区别在于他们操作的数据大小不同分别为1,2,4,8字节

指令 效果 描述
MOV S,D D<-S 传送
movb R<-I 传送字节
movw R<-I 传送字
movl R<-I 传送双字
movq R<-I 传送四字
movabsq I ,R R<-I 传送绝对的四字

在hello.s中也存在着大量的MOV数据传送,例如:

  1. 算数和逻辑操作:
指令 效果 描述
leaq S,D D<-&S 加载有效地址
INC D D<-D+1 加一
DEC D D<-D-1 减一
NEG D D<-D 取负
NOT D D<-~D 取补
ADD S,D D<-D+S
leaq S,D D<-&S 加载有效地址
SUB S,D D<-D-S
IMUL S,D D<-D*S
XOR S,D D<-D^S 异或
OR S,D D<-D|S
AND S,D D<-D&S
SAL k,D D<-D<<k 左移
SHR k,D D<-D>>(L)k 逻辑右移
控制转移
指令 同义名 跳转条件 描述
jmp Label 1 直接跳转
jmp *Operand 1 间接跳转
je Label jz ZF 相等/零
jne Label jnz ~ZF 不相等/非零
js Label SF 负数
jns Label ~SF 非负数
jg Label jnle (SF^OF)&ZF 大于(有符号>)
jge Label jnl ~(SF^OF) 大于或等于(有符号>=)
jl Label jnge SF^OF 小于(有符号<)
jle Label jng (SF^OF)|ZF 小于或等于(有符号<=)
ja Label jnbe CF&ZF 超过(无符号>)
jae Label jnb ~CF 超过或相等(无符号>=)
jb Label jnae CF 低于(无符号<)
jbe Label jna CF&ZF 低于或相等(无符号<=)

hello.s中用到跳转指令的部分:

函数操作:

hello.c中的函数:

a) int main(int argc, char *argv[])

参数传递与函数调用:内核执行c程序时调用特殊的启动例程,并将启动例程作为程序的起始地址,从内核中获取命令行参数和环境变量地址,执行main函数。

函数退出:hello.c中main函数有两个出口,第一个是当命令行参数数量不为3时输出提示信息并调用exit(1)退出main函数;第二个是命令行参数数量为3执行循环和getchar函数后return
0的方式退出函数。

函数栈帧结构的分配与释放:main函数通过pushq %rbp、movq %rsp, %rbp、subq $32,
%rsp
为函数分配栈空间,如果是通过exit函数结束main函数则不会释放内存,会造成内存泄露,但是程序如果通过return正常返回则是由指令leave即mov
%rbp,%rsp,pop %rbp恢复栈空间。

b) exit()

参数传递与函数调用:在hello.c中设置%edi值为0表示赋给exit函数第一个变量,然后通过call函数调用exit()。

hello.s中调用exit函数函数如下图:

c)printf()

参数传递与函数调用:printf参数根据字符串中的输出占位符数量来决定的。

hello.s中调用printf函数函数如下图:

如果printf函数有其他参数时,main函数按照寄存器表示参数的顺序为printf构造参数然后通过calll指令调用printf:

参数 1 2 3 4 5 6 其余
地址 %rdi %rsi %rdx %rcx %r8 %r9 被调用函数的栈帧

如果printf只有字符串作为参数则main函数设置%rdi(%edi)为格式化字符串地址,通过call函数调用puts函数。

函数返回:printf()函数返回值为printf打印的字符数。puts()函数执行成功返回非负数,执行失败返回EOF。

d) sleep()

参数传递与函数调用:main函数设置第一个参数%edi为sleepsecs。通过call指令调用sleep()函数。

函数返回:若进程/线程挂起到参数所指定的时间则返回0,若有信号中断则返回剩余秒数。

e) getchar()

参数传递与函数调用:main函数通过call指令调用getchar()函数,且getchar()函数无参数。

函数返回:getchar()函数返回值类型为int,如果成功返回用户输入的ASCII码,出错返回-1。

类型转换

类型转换分为显示和隐式。

隐式转换:隐式转换就是系统默认的、不需要加以声明就可以进行的转换数据类型自动提升,数据类型提升顺序为:

byte,short,char -->int -->long -->float -->double

显示转换:程序通过强制类型转换运算符将某类型数据转换为另一种类型。

hello中存在隐式转换。即:

int类型变量赋值float或double类型常量会发生隐式转换,根据向下取整的原则,系统会将整数部分赋值给变量。因此sleepsecs值为2。

3.4 本章小结

本章系统阐述了编译器将预处理文本文件hello.i翻译为文本文件hello.s的具体操作,主要就汇编语言伪指令、数据类型、汇编语言操作、控制转移,函数操作、类型转换六方面针对hello.s中各部分做出相应的解释说明。

第4章 汇编

4.1 汇编的概念与作用

概念:把汇编语言翻译成机器语言的过程称为汇编。

作用:汇编器(as)将hello.s翻译成机器语言指令,并把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在二进制目标文件hello.o中。

4.2 在Ubuntu下汇编的命令

Linux终端下进入hello.c所在文件,输入指令gcc -c -o hello.o hello.s,按下回车即可。

4.3 可重定位目标elf格式

首先使用readelf –a hello.o > hello.elf
生成hello.o文件的ELF格式。分析其组成各部分:

ELF头:

ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型(如可重定位、可执行或共享的)、机器类型(如x86-64)、节头部表(section
header
table)的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的头目(entry)。hello.elf
的ELF头如下:

节头部表:

节头部表记录了各节名称、类型、地址、偏移量、大小、全体大小、旗标、连接、信息、对齐信息。hello.elf节头部表如下:

重定位节:

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

在hello.o的重定位节中包含了main函数调用的puts、exit、printf、sleep、getchar函数以及全局变量sleepsecs,还有.rodata节(包含prnitf的格式串)的偏移量、信息、类型、符号值、符号名称及加数。rela.eh_frame记录了.text的信息。hello.elf的重定位节如下:

对hello.o进行重定位项目分析前应当首先理解重定位条目的结构及重定位方法。重定位条目被定义为如下的结构体:

  1. 1. typedef struct{

  2. 2. long offset; //偏移量 8字节

  3. 3. long type:32, //重定位类型 信息的后4个字节

  4. 4. symbol:32; //在符号表中的偏移量 信息的前4个字节

  5. 5. long addend; //计算重定位位置的辅助信息 8字节

  6. 6. }Elf64_Rela;

ELF定义了32种不同的重定位类型,针对hello.o中出现的两个:

1.RX86_ 64 PC32。
重定位-一个使用32位PC相对地址的引用。一个PC相对地址就是距程序计数器(PC)的当前运行时值的偏移量。当CPU执行一条使用PC相对寻址的指令时,它就将在指令中编码的32位值加上PC的当前运行时值,得到有效地址(如call指令的目标),PC值通常是下一条指令在内存中的地址。

  1. RX86_ 64 _32。
    重定位一个使用32位绝对地址的引用。通过绝对寻址,CPU直接使用在指令中编码的32位值作为有效地址,不需要进一步修改。

针对不同的类型寻址算法也不同,如下:

对hello.o中的puts函数进行重定位PC的相对引用分析:

定义puts的重定位条目为r,则其信息为:

  1. 1. {

  2. 2. r.offset = 0x1b

  3. 3. r.symbol = puts

  4. 4. r.type = R_X86_64_PC32

  5. 5. r.addend = -0x4

  6. 6. }

利用objdump获得hello.o的反汇编代码,可得出其占位符位置为:

puts占位符地址:

链接过程中链接器可以确定ADDR(s) 和 ADRR(r.symble),利用以下公式:

  1. 1. refptr = s + r.offset;

  2. 2. refaddr = ADDR(s) + r.offset;

  3. 3. *refptr = (unsigned)(ADDR(r.symbol) + r.addend - refaddr);

可以得出*refptr为0x**,执行这条指令时,CPU会执行以下的步骤:

1.将PC压入栈中
2. PC <— PC+0x**即可获得puts函数的地址,成功执行

对hello.o中的.rodata进行重定位PC的绝对引用分析:

定义其重定位条目为r,则其信息为:

  1. 1. {

  2. 2. r.offset = 0x16

  3. 3. r.symbol = .rodata

  4. 4. r.type = R_X86_64_32

  5. 5. r.addend = 0x0

  6. 6. }

这些字段告诉链接器要修改从偏移量0x16开始的绝对引用,这样会在运行时指向.rodata+0x0的位置。利用以下公式:

  1. 1. refptr = s + r.offset;

  2. 2. *refptr = (unsigned)(ADDR(r.symbol) + r.addend);

可以链接器链接时获得refptr,修改偏移量0x16处的占位符为refptr即可绝对引用,获得printf格式串的地址。

以上分别举了重定位PC相对引用的实例和重定位绝对引用的实例,其他重定位条目情况均相似。

符号表:

.symtab存放着程序中定义和引用函数和全局变量的信息。且不包含局部变量的条目。hello.elf的.symtab表如下:

4.4 Hello.o的结果解析

objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。

说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。

a) 文件内容构成:

hello_o_asm.txt中只有对文件最简单的描述,记录了文件格式和.text代码段;而hello.s中有对文件的描述,全局变量的完整描述(包括.type
.size .align 大小及数据类型)以及.rodata只读数据段。

两者均包含main函数的汇编代码,但是区别在于hello.s的汇编代码是由一段段的语句构成,同时声明了程序起始位置及其基本信息等;而hello_o_asm.txt则是由一整块的代码构成,除需要链接后才能确定的地址(此处用空白占位符填充),代码包含有完整的跳转逻辑和函数调用等。

b) 分支转移

hello_o_asm.txt包含了由操作数和操作码构成的机器语言,跳转指令中地址为已确定的实际指令地址(因为函数内部跳转无需通过链接确定);hello.s主要使用通过使用例如.L0、.L1等的助记符表示的段完成内部跳转及函数条用的逻辑。

c) 函数调用:

hello_o_asm.txt文件中,call地址后为占位符(4个字节的0),指向的是下一条地址的位置,原因是库函数调用需要通过链接时重定位才能确定地址;而hello.s中的函数调用直接是call+函数名表示。

d) 数据访问方式:

hello_o_asm.txt文件中,对.rodata中printf的格式串的访问需要通过链接时重定位的绝对引用确定地址,因此在汇编代码相应位置仍为占位符表示,对.data中已初始化的全局变量sleepsecs为0x0+%rip的方式访问;hello.s中访问方式为sleepsecs+%rip(此处的sleepsecs表示的是段名称而不是变量本身),格式串则需要通过助记符.LC0、.LC1等。两者访问参数的方式均相同,通过栈帧结构及%rbp相对寻址访问。

4.5 本章小结:

本章通过对汇编后产生的hello.o的可重定位的ELF格式的考察、对重定位项目的举例分析以及对反汇编文件与hello.s的对比,从原理层次了解了汇编这一过程实现的变化。

第5章 链接

5.1 链接的概念与作用

概念:链接是将各种代码和数据片段收集并组合称为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。

作用:链接可以执行于编译时,也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行。早期计算机系统中链接时手动执行的,在现代系统中,链接器由链接器自动执行。链接器使得分离编译成为可能。开发过程中无需将大型的应用程序组织委员一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。

5.2 在Ubuntu下链接的命令

Linux终端下进入hello.o所在文件,输入指令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/10/crtbegin.o hello.o -lc /usr/lib/gcc/x86_64-linux-gnu/10/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -z relro -o hello,按下回车即可

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

使用readelf -a hello > hello_out.elf执行获得包含hello的ELF格式的文件。

节头部表中包含了各段的基本信息,包括名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐等信息:

首先查看hello_out.elf中的程序头部分,如下图:

5.4 hello的虚拟地址空间

hello是典型的ELF可执行文件,其中信息为:

解析程序头部表对应内容及其含义:

ELF 头
段头表(可执行文件)
.text 节
.rodata节
.data节
.bss节
.symtab节
.rel.txt节
.rel.data节
.debug节
节头表

程序头部表对应的数据结构为(此处解析64位程序且主要针对hello出现的部分作出解释):

  1. 1. struct Elf64_Phdr

  2. 2. {

  3. 3. Elf64_Word p_type; /* Segment type */

  4. 4. Elf64_Word p_flags; /* Segment flags */

  5. 5. Elf64_Off p_offset; /* Segment file offset */

  6. 6. Elf64_Addr p_vaddr; /* Segment virtual address */

  7. 7. Elf64_Addr p_paddr; /* Segment physical address */

  8. 8. Elf64_Xword p_filesz; /* Segment size in file */

  9. 9. Elf64_Xword p_memsz; /* Segment size in memory */

  10. 10. Elf64_Xword p_align; /* Segment alignment */

  11. 11. };

p_type:描述段的种类,指明程序头所描述的内存段的类型、或如何解析程序头的信息,取值如下:

PT_LOAD = 1: 该段是一个可装载的内存段
PT_DYNAMIC = 2: 该段描述的是动态链接信息
PT_INTERP = 3: 该段描述的是一个以"\0"结尾的字符串,这个字符串是一个ELF解析器的路径
PT_NOTE = 4: 该段描述一个以"\n"结尾的字符串,这个字符串包含一些附加的信息
PT_PHDR = 6: 此段表示的是其自身所在的程序头部表在文件或内存中的位置和大小
PT_GNU_EH_FRAME = 0x6474e550:描述eh_frame_hdr段
PT_GNU_STACK = 0x6474e551:表示栈的可执行性
PT_GNU_RELRO = 0x6474e552:表明该段在重定位后设置为只读属性
p_offset指明该段中内容在文件中的位置,即段中内容的起始位置相对于文件开头处的偏移量
p_vaddr:指明该段中内容的起始位置在进程地址空间中的虚拟地址
p_paddr:明该段中内容的起始位置在进程地址空间中的物理地址
p_filesz:指明该段中内容在文件中的大小,也可以是0;单位是字节
p_memsz:该字段指明该段中内容在内存镜像中的大小,也可以是0;单位是字节
p_flags:该字段指明该段中内容的属性;取值如下
PF_W = (1 << 1) :表示该段是可写的
PF_R = (1 << 2) :指明该段是可读的

p_align:指明该段中内容如何在内存和文件中对齐;对于可装载的段来说,其p_vaddr和p_offset的值至少要向内存页面大小对齐;如果值为0或1,则表明没有对齐要求,否则,p_align应该是一个正整数,并且是2的幂次数;p_vaddr和p_offset在对p_align取模后应该相等;

b) 使用edb加载hello查看虚拟地址空间信息

edb加载hello可以从Data Dump中查看虚拟地址空间,程序的虚拟地址空间为如下图:

首先查看hello_out.elf中的程序头部分,如下图:

对应Data Dump中各段映射关系分别为:

1. PHDR部分:

此处PHDR表示该段具有读/执行权限,表示自身所在的程序头部表在内存中的位置为内存起始位置0x400000偏移0x40字节处、大小为0x1c0字节。

2.INTERP部分:

此处INTERP表示该段具有读权限,位于内存起始位置0x400000偏移0x200字节处,大小为0x1c个字节,记录了程序所用ELF解析器(动态链接器)的位置位于:/lib64/ld-linux-x86-64.so.2。

3. LOAD代码段:

此处LOAD表示第一个段(代码段)有读/执行访问权限,开始于内存地址0x400000处,总共内存大小是0x838个字节,并且被初始化为可执行目标文件的头0x838字节,其中其中包括ELF头、程序头部表以及.init、.text、.rodata字节。此处的LOAD表示第二个段(数据段)有读写权限,开始于内存地址0x600e10地址处,总的内存大小为0x250个字节,并用从目标文件中偏移0xe10处开始的.data中的0x24c个字节初始化。该段中剩下的4个字节对应于初始时将被初始化为0的.bss数据。

NOTE部分:

此处NOTE表示该段位于内存起始位置0x400000偏移0x21c字节处,大小为0x20个字节,该段是以‘\0’结尾的字符串,包含一些附加信息。

GUN_RELOAD部分:
此处为GNU_RELRO表示该段在重定位后设置为只读属性。

5.5 链接的重定位过程分析

利用指令objdump –d –r hello > hello_out_asm.txt 生成反汇编文件。对比于hello.o的反汇编文件,对比可得知不同点:
hello反汇编文件中包含Disassembly of section .init .plt .plt.got .text .fini,而hello.o反汇编文件只有Disassembly of section .text。对hello中的段做解释说明:

a) 文件内容:

段名称 含义
.init 程序初始化代码
.plt 位于代码段中,为动态链接中的过程链接表
.plt.got 动态链接中过程链接表PLT和全局偏移量表GOT的间接调转
.fini 程序结束执行代码

b) 函数调用:

hello.o反汇编文件中,call地址后为占位符(4个字节的0);而hello在生成过程中使用了动态链接共享库,函数调用时用到了延时绑定机制。以puts为例,简述其链接过程:

1. puts第一次被调用时程序从过程链接表PLT中进入其对应的条目
2. 第一条PLT指令通过全局偏移量表GOT中对应条目进行间接跳转,初始时每个GOT条目都指向它对应的PLT条目的第二条指令,这个简单跳转把控制传送回对应PLT条目的下一条指令
3. 把puts函数压入栈中之后,对应PLT条目调回PLT[0];
4. PLT[0]通过GOT[1]间接地把动态链接器的一个参数压入栈中,然后通过GOT[2]间接跳转进动态链接器中。动态链接器使用两个栈条目确定puts的运行时位置,用这个地址重写puts对应的GOT条目,再把控制传回给puts

第二次调用的过程:

1.puts被调用时程序从过程链表PLT中进入对应的条目;
2.通过对应GOT条目的间接跳转直接会将控制转移到puts。

c) 数据访问:

hello.o反汇编文件中,对.rodata中printf的格式串的访问需要通过链接时重定位的绝对引用确定地址,因此在汇编代码相应位置仍为占位符表示,对.data中已初始化的全局变量sleepsecs为0x0+%rip的方式访问;而hello反汇编文件中对应全局变量已通过重定位绝对引用被替换为固定地址。

5.6 hello的执行流程

ld-2.23.so!_dl_start
ld-2.23.so!_dl_init
LinkAddress!_start
ld-2.23.so!_libc_start_main
ld-2.23.so!_cxa_atexit
LinkAddress!_libc_csu.init
ld-2.23.so!_setjmp
运行 LinkAddress!main
程序终止 ld-2.23.so!exit

5.7 Hello的动态链接分析

在调用共享库函数时,编译器没有办法预测这个函数的运行时地址,因为定义它的共享模块在运行时可以加载到任意位置。正常的方法是为该引用生成一条重定位记录,然后动态链接器在程序加载的时候再解析它。GNU编译系统使用延迟绑定(lazybinding),将过程地址的绑定推迟到第一次调用该过程时。

延迟绑定是通过GOT和PLT实现的。GOT是数据段的一部分,而PLT是代码段的一部分。两表内容分别为:

PLT:PLT是一个数组,其中每个条目是16字节代码。PLT[0]是一个特殊条目,它跳转到动态链接器中。每个被可执行程序调用的库函数都有它自己的PLT条目。每个条目都负责调用一个具体的函数。

GOT:GOT是一个数组,其中每个条目是8字节地址。和PLT联合使用时,GOT[O]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。GOT[2]是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,其地址需要在运行时被解析。每个条目都有一个相匹配的PLT条目。

根据hello ELF文件可知,GOT起始表位置为0x404000,如图:

在调用dl_init之前0x404008后的16个字节均为0:

调用_start之后发生改变,0x404008后的两个8个字节分别变为:0x7f659b90e1e0、0x7f659b8f6ef0其中GOT[O](对应0x403e10)和GOT[1](对应0x7f659b90e1e0)包含动态链接器在解析函数地址时会使用的信息。包含动态链接器在解析函数地址时会使用的信息。GOT[2](对应0x7f659b8f6ef0)是动态链接器在1d-linux.so模块中的入口点。其余的每个条目对应于一个被调用的函数,改变后的GOT表如下:

GOT[2]对应部分是共享库模块的入口点,如下:

举例puts函数在调用puts函数前对应GOT条目指向其对应的PLT条目的第二条指令,如图puts@plt指令跳转的地址:

可以看出其对应GOT条目初始时指向其PLT条目的第二条指令的地址。puts函数执行后在查看此处地址:

可以看出其已经动态链接,GOT条目已经改变。

5.8 本章小结

本章了解了链接的概念作用,分析可执行文件hello的ELF格式及其虚拟地址空间,同时通过实例分析了重定位过程、加载以及运行时函数调用顺序以及动态链接过程,深入理解链接和重定位的过程。

第6章 hello进程管理

6.1 进程的概念与作用

概念:进程的经典定义就是一个执行中的程序的实例。系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量、以及打开文件描述符的集合。

作用:进程给应用程序提供的关键抽象有两种:

a) 一个独立的逻辑控制流,提供一个假象,程序独占地使用处理器。

b) 一个私有的地址空间,提供一个假象,程序在独占地使用系统内存。

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

shell作为UNIX的一个重要组成部分,是它的外壳,也是用户于UNIX系统交互作用界面。Shell是一个命令解释程序,也是一种程序设计语言。

  1. 读入命令行、注册相应的信号处理程序、初始化进程组。

  2. 通过paraseline函数解释命令行,如果是内置命令则直接执行,否则阻塞信号后创建相应子进程,在子进程中解除阻塞,将子进程单独设置为一个进程组,在新的进程组中执行子进程。父进程中增加作业后解除阻塞。如果是前台作业则等待其变为非前台程序,如果是后台程序则打印作业信息。

6.3 Hello的fork进程创建过程

首先了解进程的创建过程:父进程通过调用fork函数创建一个新的运行的子进程。新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时。子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程最大的区别在于他们有不同的id。

fork后调用一次返回两次,在父进程中fork会返回子进程的PID,在子进程中fork会返回0;父进程与子进程是并发运行的独立进程。内核能够以任何方式交替执行他们逻辑控制流中的指令。

hello的fork进程创建过程为:系统进程创建hello子进程然后调用waitpid()函数知道hello子进程结束,程序进程图如下:

6.4 Hello的execve过程

系统为hello fork子进程之后,子进程调用execve函数加载并运行可执行目标文件hello,并且带参数列表argv和环境变量列表envp,并将控制传递给main函数,以下是其详细过程:

加载器将hello中的代码和数据从磁盘复制到内存中,它创建类似于Linux
x86-64运行的虚拟内存映像,如下图所示:

加载器在程序头部表的引导下将hello的片复制到代码段和数据段。接下来加载器跳转到程序的入口点——_start函数的地址,_start函数调用系统启动函数__libc_start_main(定义在libc.so中),该启动函数初始化执行环境,处理main函数返回值,在需要的时候把控制传回给内核。

6.5 Hello的进程执行

首先了解进程执行中逻辑控制流、并发流、用户模式和内核模式、上下文切换等概念:

1.逻辑控制流:

在调试器单步执行程序时,会发现一系列的程序计数器(PC)的值,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或是包含在运行时动态链接到程序的共享对象中的指令。这个PC的值的序列叫做逻辑控制流。

2.并发流

一个逻辑流的执行在时间上与另一个流重叠,称为并发流,这两个流被称为并发地执行。多个流并发地执行的一般现象被称为并发。一个进程和其他进程轮流运行的概念被称为多任务。一个进程执行它的控制流的一部分的每一时间段叫做时间片。因此,多任务也叫时间分片。

3.用户模式和内核模式:

处理器通过某个控制寄存器中的一个模式位来提供限制一个应用可以执行的指令以及它可以访问的地址空间范围的功能。该寄存器描述了当前进程享有的特权。当设置了模式位时,进程就运行在内核模式中。一个运行在内核模式的进程可以执行指令集中的任何指令,并且可以访问系统中的任何内存为止;没有设置模式位时,进程就运行在用户模式中。用户模式的进程不允许和执行特权指令、也不允许用户模式中的进程直接引用地址空间中内核区内的代码和数据。

4.上下文切换:

内核为每个进程维持一个上下文,上下文就是内核重新启动的一个被强占的进程所需的状态。由包括通用目的寄存器、浮点寄存器、程序计数器、用户站、状态寄存器、内核栈和各种内核数据结构。

上下文切换的机制:

保存当前进程的上下文。

恢复某个先前被抢占的进程被保存的上下文。

将控制传递给这个新恢复的进程。

接下来阐述进程调度的概念及过程、用户态和核心态的转换:

在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这个决策就叫做调度,是由内核中称为调度器的代码处理的。在内和调度了一个新的进程运行后,它就抢占当前进程,并使用上文所述的上下文切换的机制将控制转移到新的进程。内核代表的用户执行系统调用时,可能会发生上下文切换;中断也有可能引发上下文切换。

通过上图所示的内核模式用户模式的切换描述用户态核心态转换的过程,在切换的第一部分中,内核代表进程A在内核模式下执行指令。然后在某一时刻,它开始代表进程B(仍然是内核模式下)执行指令。在切换之后,内核代表进程B在用户模式下执行指令。随后,进程B在用户模式下运行一会儿,直到磁盘发出一个中断信号,表示数据已经从磁盘传送到了内存。内核判定进程B已经运行了足够长的时间,就执行一个从进程B到进程A的上下文切换,将控制返回给进程A中紧随在系统调用read之后的那条指令。进程A继续运行,直到下一次异常发生,依此类推。

6.6 hello的异常与信号处理

hello执行过程中出现的异常种类可能会有:中断、陷阱、故障、终止。

中断是异步发生的,是来自处理器外部的I/O设备的信号的结果。硬件中断的异常处理程序被称为中断处理程序。

陷阱是有意的异常,是执行一条指令的结果。就像中断处理程序一样,陷阱处理程序将控制返回到下一条指令。陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。

故障由错误情况引起,它可能能够被故障处理程序修正。当故障发生时,处理器将控制转移给故障处理程序。如果处理程序能够修正这个错误情况,它就将控制返回到引起故障的指令,从而重新执行它。否则处理程序返回到内核中的abort例程,abort例程会终止引起故障的应用程序。

终止是不可恢复的致命错误造成的结果,通常是一些硬件错误,比如DRAM或者SRAM位被损坏时发生的奇偶错误。终止处理程序从不将控制返回给应用程序。处理程序将控制返回给一个abort例程,该例程会终止这个应用程序。

信号允许进程和内核中断其他进程。每种信号都对应于某种系统事件。低层的硬件异常是由内核异常处理程序处理的,正常情况下,对用户进程是不可见的。信号提供一种机制,通知用户进程发生了这些异常。例如在hello运行过程中键入回车,Ctrl-Z,Ctrl-C等,如下:

在程序运行过程中键入Ctrl-Z,会导致内核发送SIGTSTP信号给hello,同时发送SIGCHLD给hello的父进程:

键入Ctrl-Z后执行ps显示当前进程数量及内容,其中包含被暂停的hello进程,进程PID为3231:

键入Ctrl-Z后执行jobs显示当前暂停的进程,其中包含hello进程:

执行pstree显示当前进程树:


运行过程键入Ctrl-Z后执行fg,执行fg命令恢复前台作业hello:

运行过程键入Ctrl-Z后执行kill,执行kill命令杀死进程:

在hello执行过程中键入Ctrl-C终止hello进程:

6.7本章小结

本章从进程的角度描述了hello子进程fork和execve过程,并针对execve过程中虚拟内存映像以及栈组织结构等作出说明。同时简单概述了逻辑控制流中内核的调度及上下文切换等机制。阐述了Shell和Bash运行的处理流程以及hello执行过程中可能引发的异常和信号处理。

第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址空间是由段地址和偏移地址构成的。

例如:23:8048000 段寄存器(CS等16位):偏移地址(16/32/64);

实模式下:逻辑地址CS:EA —>物理地址CS*16+EA;

保护模式下:以段描述符作为下标,到GDT/LDT表查表获得段地址, 段地址+偏移地址=线性地址。

线性空间地址为非负整数地址的有序集合,例如{0,1,2,3…}。

虚拟地址空间为N = 2n 个虚拟地址的集合,例如{0,1,2,3,….,N-1}。

物理地址空间为M = 2m 个物理地址的集合,例如{0,1,2,3,….,M-1}。物理地址是真实的物理内存的地址。

Intel采用段页式存储管理(通过MMU)实现:

·段式管理:逻辑地址—>线性地址==虚拟地址;

·页式管理:虚拟地址—>物理地址。

以hello中的puts调用为例:mov $0x400714,%edi callq 4004a0,$0x400714为puts输出字符串逻辑地址中的偏移地址,需要经过段地址到线性地址的转换变为虚拟地址,然后通过MMU转换为物理地址,才能找到对应物理内存。

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

段式寄存器(16位)用于存放段选择符:CS(代码段)是指程序代码所在段;SS(栈段)是指栈区所在段;DS(数据段)是指全局静态数据所在段;其他三个段寄存器ES、GS和FS可指向任意数据段。
段选择符中各字段含义为:

其中TI表示描述符表,TI=0则为全局描述符表;TI=1则为局部描述符表。RPL表示环保护,为第0级,位于最高级的内核态,RPL=11,为第3级,位于最低级的用户态,中间环留给中间软件使用。高13位表示用来确定当前使用的段描述符在描述符表中的位置。

逻辑地址向线性地址转换的过程中被选中的描述符先被送至描述符cache,每次从描述符cache中取32位段基址,与32位段内偏移量(有效地址)相加得到线性地址,过程如下图(注意:GDT首址或LDT首址都在用户不可见寄存器中):

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

虚拟内存概念:虚拟内存是系统对主存的抽象概念,是硬件异常、硬件地址翻译、主存、磁盘文件和内存文件的完美交互。为每个进程提供了一个大的、一致的和私有的地址空间。

虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。每个字节都有一个唯一的虚拟地址,作为到数组的索引。磁盘上的数据被分割成块,这些块作为磁盘和主存(较高层)之间的传输单元。虚拟页则是虚拟内存被分割为固定大小的块。物理内存被分割为物理页,大小与虚拟页大小相同。

虚拟页物理页缓存关系:

页表是一个页表条目的数组,将虚拟页地址映射到物理页地址。

虚拟页映射物理页:

地址翻译中需要虚拟地址、物理地址的组成部分及其他基本参数,如下:
地址翻译可简化为以下流程:

形式上来说,地址翻译是一个N元素的虚拟地址空间(VAS)中的元素和一个M元素的物理地址空间(PAS)中元素的映射。CPU中存在一个控制寄存器为页表基址寄存器指向当前页表。n位的虚拟地址包含着p位的虚拟页面偏移和(n-p)位的虚拟页号。MMU利用VPN来选择适当的PTE,将页表条目中的物理页号和虚拟地址中的VPO串联起来就得到相对应的物理地址。

页命中时CPU硬件执行的步骤为:

1.处理器生成虚拟地址,并传给MMU。
2.MMU生成PTE地址,并从高速缓存/主存中请求得到它。
3.高速缓存/主存向MMU返回PTE。
4.MMU构造物理地址并把它传送给高速缓存/主存。
5.高速缓存/主存泛会所请求的数据字给处理器。

缺页时CPU硬件执行步骤为:

1.与页命中1到3相同。
2. PTE中有效位为0,MMU触发一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序。
3. 却也处理程序确定出物理内存的牺牲页,如果这个页面已经被修改了,则把他换出物理磁盘。
4. 缺页处理程序页面调入新的页面,并更新内存中的PTE。
5. 缺页处理程序返回到原来的进程,再次执行导致缺页的指令。CPU将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面现在缓存在物理内存中,所以就会命中。主存会将所请求的字返回给处理器。

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

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


用于组选择和行匹配的索引和标记字段是从虚拟地址的虚拟页号中提取出来的。如果TLB有T=2t个组,那么TLB索引是由VPN的t个最低位组成的,而TLB标记是由VPN中剩余的位组成的。

TLB中所有的地址翻译步骤都是在芯片上的MMU执行的,因此非常快。当TLB命中时,其执行步骤为:

1.CPU上产生一个虚拟地址。
2.(2和3)MMU从TLB中取出相应的PTE。
3.MMU将这个虚拟地址翻译成一个物理地址,并且将它发送到高速缓存/主存。
4.高速缓存/主存将所请求的数据字返回给CPU。

其对应的操作图为:

1.TLB命中

2.TLB不命中时,MMU需要从L1缓存取出相应的PTE,存于TLB之中,可能会覆盖已存在条目。其操作图为:

使用四级页表的地址翻译,虚拟地址被划分为4个VPN和1个VPO。每个VPNi都是一个到i级页表的索引,其中1<=i<=4.第j级页表中的每个PTE,1<=j<=3,都是指向第j+1级的每个页表的基址。第4级也表中的每个PTE包含某个物理页面的PPN,或者一个磁盘块的地址。为了构造物理地址,在能够确定PPN之前,MMU必须访问4个PTE。下图为使用Core
i7的4级页表的地址翻译:

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

如图为Core i7的内存系统:

针对物理内存访问,主要对各类高速缓存存储器的读写策略做出说明:

当CPU执行一条读内存字w的指令,它向L1高速缓存请求这个字。如果L1高速缓存由w的一个缓存的副本,那么就得到L1的高速缓存命中,高速缓存会很快抽取出w并返回给CPU。否则就是缓存不命中,当L1高速缓存向主存请求包含w的块的一个副本时,CPU必须等待。当被请求的块最终从内存到达时,L1高速缓存将这个快存放在他的一个高速缓存行里,从被缓存的块中抽取字w,然后返回给CPU。总体来看,高速缓存确定一个请求是否命中,然后抽取出被请求的字的过程,分为三步,(1)组选择(2)行匹配(3)字抽取。

  1. 直接映射高速缓存读策略:

直接映射高速缓存E=1,即每组只有一行。组选择是通过组索引位标识组。高速缓存从w的地址中间抽取出s个组索引位,这些位被解释为一个对应于一个组号的无符号整数,来进行组索引。行匹配中,确定了某个组i,接下来需要确定是否有字w的一个副本存储在组i包含的一个高速缓存行里,因为直接映射高速缓存只有一行,如果有效位为1且标志位相同则缓存命中,根据块偏移位即可查找到对应字的地址并取出;若有效位为1但标志位不同则冲突不命中,有效位为0则为冷不命中,此时都需要从存储器层次结构下一层取出被请求的块,然后将新的块存储在组索引位指示的组中的一个高速缓存行中。

  1. 组相联高速缓存读策略:

组相联高速缓存每个组都会保存多余一个的高速缓存行,组选择与直接映射高速缓存的组选择一样,通过组索引位标识组。行匹配时需要找遍组中所有行,找到标记位有效位均相同的一行则缓存命中;如果CPU请求的字不在组的任何一行中,则缓存不命中,选择替换时如果存在空行选择空行,如果不存在空行则通过替换策略替换其中一行。

  1. 全相联高速缓存读策略:

全相联高速缓存只包含一个组,其行匹配和字选择与组相联高速缓存中一样,区别主要是规模大小的问题。

写策略:分为两种,直写和写回。

直写是立即将w的高速缓存块写回到紧接着的低一层中。虽然简单,但是只写的缺点是每次写都会引起总线流量。

写回尽可能的推迟更新,只有当替换算法要驱逐这个更新过的块时,才把它写回到紧接着的第一次层中,由于局部性,写回能显著减少总线流量,但增加了复杂性。处理写不命中有两种方法一种为写分配,加载相应的的低一层的块到高速缓存中,然后更新这个高速缓存块。另一种方法为非写分配,避开高速缓存,直接把这个字写到低一层中,直写高速缓存通常是非写分配的,写回高速缓存通常是写分配的。

7.6 hello进程fork时的内存映射

首先了解共享对象在虚拟内存中的应用:

首先了解共享对象在虚拟内存中的应用:
一个对象可以被映射到虚拟内存的一个区域,要么作为共享对象,要么作为私有对象。如果一个进程将一个共享对象映射到它的虚拟地址空间的一个区域内,那么这个进程对这个区域的任何写操作,对于那些也把这个共享对象映射到它们虚拟内存的其他进程而言,也是可见的。而且,这些变化也会反映在磁盘上的原始对象中。
另一方面,对于一个映射到私有对象的区域做的改变,对于其他进程来说是不可见的,并且进程对这个区域所做的任何写操作都不会反映在磁盘上的对象中。一个映射到共享对象的虚拟内存区域叫做共享区域。类似地,也有私有区域。下图为共享对象使用实例:

其次,关注写时复制这一概念:

私有对象使用一种叫写时复制来映射至虚拟内存中,多个进程可将一个私有对象映射到其内存不同区域,共享该对象同一物理副本对于每个映射私有对象的进程,相应私有区域的页表条目都被标记为只读,并且区域结构被标记为私有的写时复制。只要没有进程试图写它自己的私有区域,它们就可以继续共享物理内存中对象的一个单独副本。然而,只要有一个进程试图写私有区域内的某个页面,那么这个写操作就会触发一个保护故障。

当故障处理程序注意到保护异常是由于进程试图写私有的写时复制区域中的一个页面而引起的,它就会在物理内存中创建这个页面的一个新副本,更新页表条目指向这个新的副本,然后恢复这个页面的可写权限。下图为写时复制的示例:

当fork函数被系统调用时,内核会为hello创建子进程,同时会创建各种数据结构并分配给hello唯一的PID。为了给hello创建虚拟内存,内核创建了当前进程的mm_struct、区域结构和样表的原样副本,并将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为写时复制。

7.7 hello进程execve时的内存映射

execve函数在hello进程中加载并运行hello,主要步骤如下:7.8 缺页故障与缺页中断处理:

  1. 删除已存在的用户区域。

  2. 映射hello私有区域。

  3. 映射共享区域。

  4. 设置程序计数器PC。

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

下一次调度hello,将从入口点开始执行。Linux根据需要换入代码和数据页面。加载器映射用户地址空间区域如下:

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

下图为Linux组织虚拟内存的结构:

缺页异常及其处理过程:如果DRAM缓存不命中称为缺页。当地址翻译硬件从内存中读对应PTE时有效位为0则表明该页未被缓存,触发缺页异常。缺页异常调用内核中的缺页异常处理程序。

缺页处理程序搜索区域结构的链表,把A和每个区域结构中的vm_start和vm_end做比较,如果指令不合法,缺页处理程序就触发一个段错误、终止进程。

缺页处理程序检查试图访问的内存是否合法,如果不合法则触发保护异常终止此进程。

缺页处理程序确认引起异常的是合法的虚拟地址和操作,则选择一个牺牲页,如果牺牲页中内容被修改,内核会将其先复制回磁盘。无论是否被修改,牺牲页的页表条目均会被内核修改。接下来内核从磁盘复制需要的虚拟页到DRAM中,更新对应的页表条目,重新执行导致缺页的指令。以下为Linux缺页处理简图:

7.9动态存储分配管理

动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为 一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已 分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器有两种基本风格,都要求应用显式地分配块,它们的不同之处在于由哪个实体来负责释放已分配的块。

隐式空闲链表的堆块格式及其组织格式如下图:
隐式链表堆块的格式:头部(字,块大小+标志位)+ 有效荷载+ 填充:

隐式链表组织格式:

隐式空闲链表空闲块是通过头部中的大小字段隐含地连接着的。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。

显式空闲链表的堆块格式如下图:

显式空闲链表有两种方式来维护一种是先进后出,另一种是地址顺序。放置空闲块的策略有三种,分别是首次适配、下一次适配、最佳适配。首次适配从头开始搜索空闲链表,选择第一个合适的空闲块。下一次适配和首次适配很相似,只不过不是从链表的起始处开始每次搜索,而是从上一.次查询结束的地方开始。最佳适配检查每个空闲块,选择适合所需请求大小的最小空闲块。

7.10本章小结

本章从Linux存储器的地址空间起,阐述了Intel的段式管理和页式管理机制,以及TLB与多级页表支持下的VA到PA的转换,同时对cache支持下的物理内存访问做了说明。针对内存映射及管理,简述了hello的fork和execve内存映射了解了缺页故障与缺页中断处理程序,对动态分配管理做了系统阐述。

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

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

8.2 简述Unix IO接口及其函数

1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备。
2.Linux Shell创建的每个进程开始时都有三个打开的文件:标准输入、标准输出、标准错误。
3.改变当前文件的位置。对于每个打开的文件,内核保持着一个文件位置k、初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek操作,显示地设置文件的当前位置为k。
4.读写文件。一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。写操作就是从内存复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
5.关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。

8.3 printf的实现分析

首先查看printf函数的函数体:

  1. 1. static int printf(const char *fmt, …)

  2. 2. {

  3. 3. va_list args;

  4. 4. int i;

  5. 5. va_start(args, fmt);

  6. 6. write(1,printbuf,i=vsprintf(printbuf, fmt, args));

  7. 7. va_end(args);

  8. 8. return i;

  9. 9. }

va_list的定义是:typedef char *
va_list,因此通过调用va_start函数,获得的arg为第一个参数的地址。vsprintf的作用是格式化。接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出,例如hello中:printf(“Hello
%s %s\n”,argv[1],argv[2]);命令行参数为./hello 1190201224
徐亚楠\n,则对应格式化后的字符串为:Hello 1173710217
hpy\n,并且i为返回的字符串长度。

接下来是write函数:

  1. 1. write:

  2. 2. mov eax, _NR_write

  3. 3. mov ebx, [esp + 4]

  4. 4. mov ecx, [esp + 8]

  5. 5. int INT_VECTOR_SYS_CALL

根据代码可知内核向寄存器传递几个参数后,中断调用syscall函数。对应ebx打印输出的buf数组中第一个元素的地址,ecx是要打印输出的个数。查看syscall函数体:

  1. 1. sys_call:

  2. 2. call save

  3. 4. push dword [p_proc_ready]

  4. 6. sti

  5. 8. push ecx

  6. 9. push ebx

  7. 10. call [sys_call_table + eax * 4]

  8. 11. add esp, 4 * 3

  9. 13. mov [esi + EAXREG - P_STACKBASE], eax

  10. 14. cli

  11. 15. ret

在syscall函数中字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量),实现printf格式化输出。

8.4 getchar的实现分析

getchar源代码为:

  1. 1. int getchar(void)

  2. 2. {

  3. 3. static char buf[BUFSIZ];

  4. 4. static char *bb = buf;

  5. 5. static int n = 0;

  6. 6. if(n == 0)

  7. 7. {

  8. 8. n = read(0, buf, BUFSIZ);

  9. 9. bb = buf;

  10. 10. }

  11. 11. return(–n >= 0)?(unsigned char) *bb++ : EOF;

  12. 12. }

异步异常-键盘中断的处理:键盘中断处理是底层的硬件异常,当用户按下键盘时,内核会调用异常键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。getchar函数read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。实现读取一个字符的功能

8.5本章小结

本章系统的了解了Unix I/O,通过LinuxI/O设备管理方法以及Unix I/O接口及函数了解系统级I/O的底层实现机制。通过对printf和getchar函数的底层解析加深对Unix I/O以及异常中断等的了解。

结论

1. hello.c首先经过预处理器处理,得到预处理文本文件hello.i
2.hello.i经过编译器处理生成文本文件hello.s,包含一个汇编程序
3. hello.s经过汇编器翻译为机器语言指令,打包为可重定位目标程序hello.o
4.hello经过链接生成可执行目标文件hello
5.在Linux下键入./hello 1190201224 徐亚楠运行hello,内核为hello fork出新进程,并在新进程中execve hello程序
6.execve 通过加载器将hello中的代码和数据从磁盘复制到内存,为其创建虚拟内存映像,加载器在程序头部表的引导下将hello的片复制到代码段和数据段,执行_start函数
7.MMU通过页表将虚拟地址映射到对应的物理地址完成访存
8.内核通过GOT和PLT协同工作完成共享库函数的调用
9.hello调用函数(eg:printf),内核通过动态内存分配器为其分配内存
10.内核通过调度完成hello和其他所有进程的上下文切换,成功运行hello
11.shell父进程回收hello,内核删除hello进程的所有痕迹

总结:
就像Π一样,圆周长与其直径之比,这是开始,后面一直有 无穷无尽 永不重复,就是说在这串数字中,包含每种可能的组合,你的生日,储物柜密码,你的社保号码,都在其中某处,如果把这些数字转换为字母,就能得到所有的单词无数种组合,你婴儿时发出的第一个音节,你心上人的名字,你一辈子从始至终的故事,我们做过或说过的每件事,宇宙中所有无限的可能,都在这个简单的圆中,用这些信息做什么,它有什么用,取决于我们。就像hello一句简单的问候也只是开始。
回顾hello短暂的一生,hello虽然很简单,但是却凝结着人类的智慧。自1946年第一台电子计算机问世以来,计算机技术在元件器件、硬件系统结构、软件系统、应用等方面,均有惊人进步。从两个足球场大的计算机到如今我们面前的小小的笔记本,不得不令人感叹现代计算机系统设计的精巧。
计算机系统高效有序的运行离不开底层硬件的完美契合,计算机多级存储结构、内核对进程的调度策略、动态链接的执行方式、cache替换策略、页表替换策略、异常与信号等的处理……这些无一不体现计算机底层实现的完备与优雅。同时,作为程序员,了解与学习计算机底层实现也有助于我们充分利用计算机,编写出计算机底层友好的代码,提高计算、工作的效率。最底层硬件CPU的设计人员,在上层的显示等效与更高效率的要求下,巧妙地构造出了流水线处理的模式;存储系统的设计人员,在实现读写等效的效果下,为了更好更省地让存储系统跟上CPU的速度,构想出了多级缓存结构;操作系统的设计人员,为了构建复杂的机器操作与人类的思维逻辑操作间的桥梁,设计出了诸如linux、windows等操作系统…每一层的实现者,为了上一层人员实现的便利,付出了无数的智慧与汗水,正是因为他们,才有了精密而美妙的计算机系统,才能让我们这些初学者“轻易”实现一个实则复杂无比的操作!

总之,hello的故事,其实才刚刚开始……

附件

1.hello.c:源代码
2.hello.i:hello.c预处理生成的文本文件
3. hello.s:hello.i经过编译器翻译成的文本文件hello.s,含汇编语言程序
4. hello.o:hello.s经汇编器翻译成机器语言指令打包成的可重定位目标文件
5. hello.elf:hello.o的ELF格式
6. hello_o_asm.txt:hello.o反汇编生成的代码。
7. hello:经过hello.o链接生成的可执行目标文件。
8.hello_out.elf:hello的ELF格式。
9. hello_out_asm.txt:hello反汇编生成的代码。

参考文献

[1]兰德尔E.布莱恩特 大卫R.奥哈拉伦. 深入理解计算机系统(第3版).机械工业出版社. 2018.4.

[2]条件编译#ifdef的妙用详解_透彻:https://blog.csdn.net/qq_33658067/article/details/79443014

[3]typedef 百度百科:https://baike.baidu.com/item/typedef/9558154?fr=aladdin

[4]extern 百度百科:https://baike.baidu.com/item/extern/4443005?fr=aladdin

[5]编译 百度百科:https://baike.baidu.com/item/编译/1258343?fr=aladdin

[6]printf函数详细讲解:https://www.cnblogs.com/windpiaoxue/p/9183506.html

[7] 关于Linux
中sleep()函数说明:https://blog.csdn.net/fly__chen/article/details/53175301

[8]
getchar()函数的返回值以及单个字符输出函数putchar:https://blog.csdn.net/cup160828/article/details/58067647?utm_source=blogxgz9

[9]gcc常用命令选项:https://blog.csdn.net/Crazy_Tengt/article/details/71699029

[10]ELF文件-段和程序头:https://blog.csdn.net/u011210147/article/details/54092405

[11]printf函数实现的深入剖析:https://www.cnblogs.com/pianist/p/3315801.html

[12]getchar()函数详解:http://www.aspku.com/kaifa/c/332134.html

HIT CSAPP 程序人生-Hello’s P2P相关推荐

  1. HIT CSAPP——程序人生-Hello’s P2P

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 航天学院 学   号 120L012012 班   级 2036015 学       生 崔耕硕 指 导 教 ...

  2. HIT CSAPP 程序人生(Hello‘s P2P)

    计算机系统大作业 题  目  程序人生-Hello's P2P 专  业  计算机科学与技术 学  号  ********** 班  级  ******* 学  生  陈一豪 指导教师  刘宏伟 计算 ...

  3. csapp 程序人生 Hello’s P2P

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

  4. HIT CSAPP程序人生大作业

  5. hit csapp大作业 程序人生-Hello’s P2P

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算机科学与技术 学   号 2021112810 班   级 2103103 学       生 肖芩芩 指 ...

  6. HIT 深入理解计算机系统 大作业 程序人生-Hello’s P2P

    HIT 深入理解计算机系统 大作业 程序人生-Hello's P2P 本论文旨在研究 hello 在 linux 系统下的整个生命周期.结合 CSAPP 课本, 通过 gcc 等工具进行实验,从而将课 ...

  7. HIT CS:APP 计算机系统大作业 《程序人生-Hello’s P2P》

    HIT CS:APP 计算机系统大作业 程序人生-Hello's P2P Hello的自白 我是Hello,我是每一个程序猿¤的初恋(羞羞--) l却在短短几分钟后惨遭每个菜鸟的无情抛弃(呜呜--), ...

  8. HIT计算机系统大作业-程序人生-Hello’s P2P

    计算机系统大作业 ** 由于采用静态部署,需要看图片详细分析的小伙伴请移步个人博客网站:** 个人博客 题目:程序人生-Hello's P2P 学号: 姓名:熊峰 摘要: hello程序作为最简单的. ...

  9. 哈工大CSAPP大作业:程序人生-Hello’s P2P

    ​     计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算学部 学    号 120L021818 班    级 2003006 学       生 秦 ...

最新文章

  1. 我们的解决方案:日志系统
  2. Shell排序的原理与集体实现
  3. 高通---GPUCPU频率和工作模式的获取设置
  4. python3与Beautiful Soup库
  5. halcon区域腐蚀膨胀算子_Halcon算子
  6. Qt学习之Qt基础入门(下)
  7. springboot-2-ioc
  8. Java-创建图片验证码descriptor
  9. 使用paramiko在eNSP的交换机中批量创建VLAN
  10. CSDN下载码如何使用?
  11. 公司让微信加人有没有软件_没有人想要使用软件
  12. pytorch 机器翻译 seq2seq 模型和注意力机制
  13. python微信朋友圈分享功能_利用Python让你的微信朋友圈与众不同,更加高大上
  14. 擦地机器人排行榜_拖地扫地机器人十大品牌排行榜哪个牌子好
  15. CJBorderView 一种iOS开发中常用的虚线边框View------* An easy way to init a view with dotted line border
  16. android百度日语输入法下载,百度日文输入法
  17. javac -d . hello.java中的 -d .是什么作用?
  18. 工信部首次发声:培育一批进军元宇宙等新兴领域的创新型中小企业
  19. 【研究】我是在做算法还是在调参?
  20. 卷积神经网络概念及使用 PyTorch 简单实现

热门文章

  1. coreldraw x8段落_在CorelDRAW中怎样使段落文本绕图
  2. WP Super Cache 安装与设置详解
  3. c语言规定整型变量长度,C语言整型变量所占内存字节数
  4. SaaS新风口——2020年中国企业直播应用场景趋势报告
  5. Java多线程编程模式实战指南一:Active Object模式(上)
  6. 【SpringBoot】SpringBoot基础
  7. 北大青鸟:手机软件开发指引
  8. 在普通应用中 计算机和电视能够很容易,计算机技术在广播电视工程中的应用要点及发展趋势...
  9. PS饰品美工,淘宝珠宝首饰美工修图…
  10. Flash AIR App在苹果上发布的经验总结