哈工大计算机系统大作业(2022)

(论文封面略去)

本人博客链接(防扒):

本人CSDN博客:

何以牵尘的博客_CSDN博客-哈工大课内学习,哈工大精品课程笔记领域博主何以牵尘擅长哈工大课内学习,哈工大精品课程笔记,等方面的知识https://blog.csdn.net/m0_61753302本人博客园博客:

何以牵尘 - 博客园 (cnblogs.com)https://www.cnblogs.com/kalesky/

摘  要

本论文研究了hello.c这一简单c语言文件在Linux系统下的整个生命周期。从原始程序开始,依次深入实践了编译、链接、加载、运行、终止、回收的完整过程,从而对hello.c文件的“一生”有了更详细的认识。该论文以hello.c文件为研究对象,结合《深入理解计算机系统》书中的内容与课上老师的讲授,在Ubuntu系统下对hello程序的整个生命周期进行了研究,通过对hello.c程序的深入研究,把本学期计算机系统课程所学知识梳理与回顾了一遍,加深了对计算机系统的了解。

关键词:计算机系统;计算机体系结构;程序的生命周期;深入理解计算机系统                           

目录

第1章 概述

1.1 Hello简介

1.2 环境与工具

1.3 中间结果

1.4 本章小结

第2章 预处理

2.1 预处理的概念与作用

2.2在Ubuntu下预处理的命令

2.3 Hello的预处理结果解析

2.4 本章小结

第3章 编译

3.1 编译的概念与作用

3.2 在Ubuntu下编译的命令

3.3 Hello的编译结果解析

3.3.1文件结构分析

3.3.2数据和赋值

3.3.3算术操作

3.3.4控制转移操作

3.3.5数组/指针/结构操作

3.3.6函数操作

3.4 本章小结

第4章 汇编

4.1 汇编的概念与作用

4.2 在Ubuntu下汇编的命令

4.3 可重定位目标elf格式

4.3.1命令

4.3.2结构分析

4.4 Hello.o的结果解析

4.4.1命令

4.4.2对照分析

4.5 本章小结

第5章 链接

5.1 链接的概念与作用

5.2 在Ubuntu下链接的命令

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

5.4 hello的虚拟地址空间

5.5 链接的重定位过程分析

5.6 hello的执行流程

5.7 Hello的动态链接分析

5.8 本章小结

第6章 hello进程管理

6.1 进程的概念与作用

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

6.3 Hello的fork进程创建过程

6.4 Hello的execve过程

6.5 Hello的进程执行

6.6 hello的异常与信号处理

6.7本章小结

第7章 hello的存储管理

7.1 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动态存储分配管理

7.10本章小结

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

8.2 简述Unix IO接口及其函数

8.3 printf的实现分析

8.4 getchar的实现分析

8.5本章小结

结论

附件

参考文献



第1章 概述

1.1 Hello简介

        根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。

(1)P2P,即Program to Process

        是指hello.c文件从可执行程序(Program)变为运行时进程(Process)的过程。

在Linux系统下,hello.c 文件依次经过:

① cpp(C Pre-Processor,C预处理器)预处理

② ccl(C Compiler,C编译器) 编译

③ as (Assembler,汇编器)汇编

④ ld (Linker,链接器)链接

        最终成为可执行目标程序hello(在Linux下该文件无固定后缀)。

       打开shell,输入命令./hello后,shell 通过fork产生子进程,hello 便从可执行程序(Program)变成为进程(Process)。

(2)020,指hello.c文件的“From 0 to 0”

① “From 0”:初始时内存中并无hello文件的相关内容。

② “to 0”:通过在Shell下调用execve函数,系统会将hello文件载入内存,执行相关代码,当程序运行结束后, hello进程被回收,并由内核删除hello相关数据。

1.2 环境与工具

硬件环境:

        处理器:Intel(R) Core(TM) i7-1065G7 CPU @ 1.30GHz   1.50 GHz

        RAM:16GB

        系统类型:64位操作系统,基于x64的处理器

软件环境:

        Windows10 64位;Ubuntu 20.04.3 LTS amd64

调试工具:

        Visual Studio 2022 64-bit;Visual Studio Code;

        gedit,gcc,notepad++,readelf, objdump, hexedit, edb

1.3 中间结果

文件名

功能

hello.i

预处理后得到的文本文件

hello.s

编译后得到的汇编语言文件

hello.o

汇编后得到的可重定位目标文件

hello.elf

用readelf读取hello.o得到的ELF格式信息

hello.asm

反汇编hello.o得到的反汇编文件

hello1.elf

由hello可执行文件生成的.elf文件

hello1.asm

反汇编hello可执行文件得到的反汇编文件

表格 1 中间结果

1.4 本章小结

        本章对hello进行了一个总体的概括,首先介绍了P2P、020的意义和过程,介绍了作业中的硬件环境、软件环境和开发工具,最后简述了从.c文件到可执行文件中间经历的过程。


第2章 预处理

2.1 预处理的概念与作用

(1)预处理的概念

        预处理步骤是指程序开始运行时,预处理器(cpp,C Pre-Processor,C预处理器)根据以字符#开头的命令,修改原始的C程序的过程,其中 ISO C/C++要求支持的包括#if、 #ifdef、 #ifndef、 #else、 #elif、 #endif(条件编译)、 #define(宏定义)、 #include(源文件包含)、 #line(行控制)、 #error(错误指令)、 #pragma(和实现相关的杂注)以及单独的#(空指令)。除此之外,预处理过程还会删除程序中的注释和多余的空白字符。预处理通常得到另一个以.i作为拓展名的C程序。

      例如,hello.c文件6到8行中的#include 命令会告诉预处理器读取系统头文件stdio.h,unistd.h,stdlib.h 的内容,并把这些内容直接插入到程序文本中。用实际值替换用#define定义的字符串。

(2)预处理的作用

        预处理过程将#include后继的头文件内容直接插入程序文本中,完成字符串的替换,方便后续处理。预处理过程中并未直接解析程序源代码的内容,而是对源代码进行相应的分割、处理和替换。简单来说,预处理是一个文本插入与替换的过程,生成的hello.i文件仍然是文本文件。

2.2在Ubuntu下预处理的命令

在Ubuntu系统下,对hello.c进行预处理的命令为:

cpp hello.c > hello.i

运行截图如下:

图 1 预处理过程

2.3 Hello的预处理结果解析

       在Linux下打开hello.i文件,发现hello.i文件已经扩展到3063行,行数较hello.c文件有大幅度增加。原来hello.c中的main函数相关部分在hello.i中对应着3047行到3063行。

图 2 预处理结果部分展示

       在main函数代码出现之前的大段代码源自于的头文件 <stdio.h> <unistd.h> <stdlib.h> 的依次展开。

       以 stdio.h 的展开为例:cpp先删除指令#include <stdio.h>,并到Ubuntu系统默认的环境变量中寻找 stdio.h,最终打开路径/usr/include/stdio.h下的stdio.h文件。若stdio.h文件中使用了#define语句,则按照上述流程继续递归展开,直到所有#define语句都被解释替换掉为止,所以最终的.i 文件中是没有#define 的。若发现其中使用了大量的#ifdef #ifndef条件编译的语句,cpp会对条件值进行判断来决定是否执行包含其中的逻辑。除此之外,cpp还会进行删除程序中的注释和多余的空白字符等操作,并对一些值进行替换。

2.4 本章小结

       本章主要介绍了预处理(包括头文件的展开、宏替换、去掉注释、条件编译)的概念及作用,以及Linux下预处理的两个指令,同时结合Ubuntu系统下hello.c文件实际预处理之后得到的hello.i程序并对预处理结果进行了解析,详细了解了预处理的内涵。


第3章 编译

3.1 编译的概念与作用

(1)编译的概念

       编译是指C编译器ccl通过词法分析和语法分析,将合法指令翻译成等价汇编代码的过程。通过编译过程,编译器将文本文件 hello.i 翻译成汇编语言文件 hello.s,在hello.s中,以文本的形式描述了一条条低级机器语言指令。

(2)编译的作用

       将文本文件翻译成汇编语言程序,为后续将其转化为二进制机器码做准备。

   

3.2 在Ubuntu下编译的命令

在Ubuntu系统下,对hello.i进行编译的命令为:

gcc -S hello.i -o hello.s

运行截图如下:

图 3 编译过程

3.3 Hello的编译结果解析

3.3.1文件结构分析

对hello.s中出现的文件结构进行分析:

内容

含义

.file

源文件

.text

代码段

.global

全局变量

.data

已初始化的全局和静态变量

.section  .rodata

只读变量

.align

对齐方式

.type

函数类型/对象类型

.size

文件大小

.long  .string

long类型/string类型

表格 2 hello.s文件结构

3.3.2数据和赋值

(1)常量

①if语句中的常量

在if语句中

 常量4的值保存在.text中,作为指令的一部分

②for循环语句中的常量

在for语句中

 其中数字0、8、1、2、3也存储在.text节中

③函数中的字符串

在下列函数中:

 printf()、scanf()中的字符串呗存储在.rodata节中

(2)变量

①全局变量

       已初始化的全局变量被存储在.data节中,它的初始化没有汇编指令,而是直接完成的。

②局部变量

       局部变量被存储在寄存器或栈中。hello.c程序中的局部变量i:

在汇编代码中:

        此处是在for循环前令i = 0的操作,可以看到i被保存在栈中%rsp - 4的位置上。

(3)赋值操作

       对局部变量的赋值在汇编代码中通过mov指令完成。具体使用哪条mov指令由数据的大小决定,如表格所示:

后缀

b

w

l

q

大小(字节)

1

2

3

4

表格 3 mov指令的后缀

3.3.3算术操作

汇编语言中,算数操作的指令包括:

指令

效果

leaq s,d

d=&s

inc d

d+=1

dec d

d-=1

neg d

d=-d

add s,d

d=d+s

sub s,d

d=d-s

imulq s

r[%rdx]:r[%rax]=s*r[%rax](有符号)

mulq s

r[%rdx]:r[%rax]=s*r[%rax](无符号)

idivq s

r[%rdx]=r[%rdx]:r[%rax] mod s(有符号) r[%rax]=r[%rdx]:r[%rax] div s

divq s

r[%rdx]=r[%rdx]:r[%rax] mod s(无符号) r[%rax]=r[%rdx]:r[%rax] div s

表格 4 算数操作指令

       在for循环中,使用了自加操作符“++”:

       该操作的方式为:在每次循环执行的内容结束后,对i自身进行一次自加1,即栈上存储变量i的值加1。在汇编代码中:

  

3.3.4控制转移操作

if语句中有判断传入参数argc是否等于4,源代码为:

       相应汇编代码为:

        je用于判断cmpl产生的条件码,若两个操作数的值不相等则跳转到指定地址;

       for循环中的循环执行条件中也有判断语句:

        相应汇编代码为:

       jle用于判断cmpl产生的条件码,若后一个操作数的值小于等于前一个则跳转到指定地址。

3.3.5数组/指针/结构操作

       主函数main的参数中有指针数组char *argv[]

        在 argv[] 中,argv[0]指向输入程序的路径和名称,argv[1]和argv[2]分别表示两个字符串。因为char* 数据类型占8个字节,根据:

.LFB6:

    .cfi_startproc

    endbr64

    pushq   %rbp

    .cfi_def_cfa_offset 16

    .cfi_offset 6, -16

    movq    %rsp, %rbp

    .cfi_def_cfa_register 6

    subq    $32, %rsp

    movl    %edi, -20(%rbp) //argc存储在%edi

    movq    %rsi, -32(%rbp) //argv存储在%rsi

.L4:

    leaq    .LC1(%rip), %rdi

    movl    $0, %eax

    call    printf@PL

.LC1:

    .string "Hello %s %s\n"

    .text

    .globl  main

    .type   main, @function

       对比原函数可知通过%rsi-8和%rax-16,分别得到argv[1]和argv[2]两个字符串。

3.3.6函数操作

       在x86-64系统中,可以通过寄存器最多传递6个整型(即整数和指针)参数。第1~6个参数一次储存在%rdi、%rsi、%rdx、%rcx、%r8、%r9这六个寄存器中,剩下的参数保存在栈当中。

1

2

3

4

5

6

7

%rdi

%rsi

%rdx

%rcx

%r8

%r9

栈空间

表格 5 64位系统的传参顺序

① main函数

参数传递:传入参数argc和argv[],分别用寄存器%rdi和%rsi存储。

函数调用:被系统启动函数调用。

函数返回:设置%eax为0并且返回,对应return 0 。

源代码:

 汇编代码:

        可以发现 argc 存储在%edi中,argv 存储在%rsi中。

② printf函数

       参数传递:

       call puts时只传入了字符串参数首地址;for循环中call  printf时传入了 argv[1]和argc[2]的地址。

       函数调用:if判断满足条件后调用,与for循环中被调用。

(1)第一处调用

源代码:

 汇编代码:

.LC0:

    .string "\347\224\250\346\263\225: Hello \345\255\246\345\217\267 \345\247\223\345\220\215 \347\247\222\346\225\260\357\274\201"

.LFB6:

    cmpl    $4, -20(%rbp)

    je  .L2

    leaq    .LC0(%rip), %rdi

    call    puts@PLT

(2)第二处调用

源代码:

 汇编代码:

.L4:

    movq    -32(%rbp), %rax

    addq    $16, %rax

    movq    (%rax), %rdx

    movq    -32(%rbp), %rax

    addq    $8, %rax

    movq    (%rax), %rax

    movq    %rax, %rsi

    leaq    .LC1(%rip), %rdi

    movl    $0, %eax

    call    printf@PLT

③ exit函数

       参数传递:传入的参数为1,再执行退出命令

       函数调用:if判断条件满足后被调用.

源代码:

 汇编代码:

.LFB6:

    movl    $1, %edi

    call    exit@PLT

④ sleep函数

       参数传递:传入参数atoi(argv[3]),

       函数调用:for循环下被调用,call sleep

源代码:

 汇编代码:

.L4:

    movq    -32(%rbp), %rax

    addq    $24, %rax

    movq    (%rax), %rax

    movq    %rax, %rdi

    call    atoi@PLT

    movl    %eax, %edi

    call    sleep@PLT

⑤ getchar函数

       函数调用:在main中被调用,call getchar

源代码:

汇编代码:

.L3:

    call    getchar@PLT

3.4 本章小结

       本章主要介绍了编译的概念以及过程,编译是将文本文件翻译成汇编语言程序,为后续将其转化为二进制机器码做准备的过程。并且以hello.s文件为例,介绍了编译器如何处理各个数据类型以及各类操作,验证了大部分数据、操作在汇编代码中的实现。通过对编译的结果进行解析,我更深刻地了解了C语言的数据与操作,对C语言翻译成汇编语言有了更深入的认识。


第4章 汇编

4.1 汇编的概念与作用

(1)汇编的概念

       汇编是指汇编器(assembler)将以.s结尾的汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并把结果保存在以.o结尾的目标文件中的过程。

(2)汇编的作用

       汇编就是将高级语言转化为机器可直接识别执行的代码文件的过程,汇编器将.s 汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式。

       .o 文件是一个二进制文件,它包含程序的指令编码。

4.2 在Ubuntu下汇编的命令

在Ubuntu系统下,对hello.s进行汇编的命令为:

gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o

运行截图如下:

图 4 汇编的命令

4.3 可重定位目标elf格式

4.3.1命令

在shell中输入readelf -a hello.o > hello.elf 指令获得 hello.o 文件的 ELF 格式:

图  5 生成ELF文件

4.3.2结构分析

(1)ELF头(ELF Header)

以 16字节序列 Magic 开始,其描述了生成该文件的系统的字的大小和字节顺序,ELF 头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括 ELF 头大小、目标文件类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量等相关信息。

 图 6 ELF头的内容

(2)节头

描述了.o文件中出现的各个节的意义,包括节的类型、位置、所占空间大小等信息。

 图 7 节头的情况

(3)重定位节

表述了各个段引用的外部符号等,在链接时,需要通过重定位节对这些位置的地址进行修改。链接器会通过重定位条目的类型判断需要使用的相应方法去计算正确的地址值。

① .rela.text

一个.text 节中位置的列表,包含.text 节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。

.rela.text节包含如下信息:

偏移量

代表需要进行重定向的代码在.text或.data节中的偏移位置

信息

包括symbol和type两部分,其中symbol占前半部分,type占后半部分,symbol代表重定位到的目标在.symtab中的偏移量,type代表重定位的类型

类型

重定位到的目标的类型

加数

计算重定位位置的辅助信息

表格 6 .rela.text节包含的信息

 图 8 .rela.text节

② .rela.eh_frame

 图 9 .rela.eh_frame节

(4)符号表(Symbol table)

.symtab是一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。

 图 10 符号表的情况

4.4 Hello.o的结果解析

4.4.1命令

在shell中输入 objdump -d -r hello.o > hello.asm 指令输出hello.o的反汇编文件,并与第3章的hello.s文件进行对照分析。

 图 11 生成hello.asm文件

4.4.2对照分析

(1)操作数的表示

        hello.s中的操作数时十进制,

        ello.o的反汇编代码中的操作数是十六进制。

(2)全局变量的访问

       在hello.s 文件中,使用段名称+%rip访问 rodata(printf 中的字符串),

       在反汇编得到的hello.asm中,则使用 0+%rip进行访问。

       原因:rodata 中数据地址在运行时才能确定,因此在访问时也需要进行重定位。在汇编成为机器语言时,将操作数设置为全0并添加对应的重定位条目。

图 12 全局变量访问

(3)分支转移

       在hello.s中,跳转指令的目标地址直接记为段名称,如.L2,.L3等。

       在反汇编得到的hello.asm中,跳转的目标为具体的地址,在机器代码中体现为目标指令地址与当前指令下一条指令的地址之差。

 图 13 分支转移

(4)函数调用

       在hello.s文件中,call之后直接跟着函数名称

       在反汇编得到的hello.asm中,call 的目标地址是当前指令的下一条指令。

原因:

       hello.c中调用的函数是共享库中的函数,最终需要通过动态链接器作用才能确定函数的运行时执行地址,在汇编成为机器语言的时候,对于这些不确定地址的函数调用,会先将其对应的call指令后的相对地址设置为全0(此时,目标地址正是下一条指令),然后在.rela.text 节中为其添加重定位条目,等待静态链接进一步确定。

 图 14 函数调用

4.5 本章小结

       本章对汇编的概念与作用进行了介绍。以hello.s文件在Ubuntu系统下通过将hello.s文件汇编为hello.o文件,并生成hello.o的ELF格式文件hello.elf为例,研究了ELF格式文件的具体结构。通过比较hello.o的反汇编代码(保存在hello.asm中)与hello.s的异同,令人深刻认识到了从汇编语言到机器语言实现的转变,和在这个过程中机器为链接所做出的准备。


5链接

5.1 链接的概念与作用

(1)链接的概念

       链接是指通过链接器(Linker),将程序编码与数据块收集并整理成为一个单一文件,生成完全链接的可执行目标文件的过程。

(2)链接的作用

       将为了节省源程序空间而未编入的常用函数文件(如printf.o)进行合并,令分离编译成为可能,减少整体文件的复杂度与大小,增加容错性,同时方便对某一模块进行针对性修改

5.2 在Ubuntu下链接的命令

在Ubuntu系统下,链接的命令为:

ld -o hello -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o hello.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o

运行截图如下:

 图 15 链接的命令

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

       分析hello的ELF格式,

       用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。

       在Shell中输入命令 readelf -a hello > hello1.elf 生成 hello 程序的 ELF 格式文件,保存为hello1.elf(与第四章中的elf文件作区分):

 图 16 生成hello的ELF文件

       打开hello1.elf,分析hello的ELF格式如下:

(1)ELF头(ELF Header)

       hello1.elf中的ELF头与hello.elf中的ELF头包含的信息种类基本相同,以描述了生成该文件的系统的字的大小和字节顺序的16字节序列Magic开始,剩下的部分包含帮助链接器语法分析和解释目标文件的信息。与hello.elf相比较,hello1.elf中的基本信息未发生改变(如Magic,类别等),而类型发生改变,程序头大小和节头数量增加,并且获得了入口地址。

 图 17 ELF头的情况

(2)节头

       描述了各个节的大小、偏移量和其他属性。链接器链接时,会将各个文件的相同段合并成一个大段,并且根据这个大段的大小以及偏移量重新设置各个符号的地址。

节头:

  [] 名称              类型             地址              偏移量

       大小              全体大小          旗标   链接   信息   对齐

  [ 0]                   NULL             0000000000000000  00000000

       0000000000000000  0000000000000000           0     0     0

  [ 1] .interp           PROGBITS         00000000004002e0  000002e0

       000000000000001c  0000000000000000   A       0     0     1

  [ 2] .note.gnu.propert NOTE             0000000000400300  00000300

       0000000000000020  0000000000000000   A       0     0     8

  [ 3] .note.ABI-tag     NOTE             0000000000400320  00000320

       0000000000000020  0000000000000000   A       0     0     4

  [ 4] .hash             HASH             0000000000400340  00000340

       0000000000000038  0000000000000004   A       6     0     8

  [ 5] .gnu.hash         GNU_HASH         0000000000400378  00000378

       000000000000001c  0000000000000000   A       6     0     8

  [ 6] .dynsym           DYNSYM           0000000000400398  00000398

       00000000000000d8  0000000000000018   A       7     1     8

  [ 7] .dynstr           STRTAB           0000000000400470  00000470

       000000000000005c  0000000000000000   A       0     0     1

  [ 8] .gnu.version      VERSYM           00000000004004cc  000004cc

       0000000000000012  0000000000000002   A       6     0     2

  [ 9] .gnu.version_r    VERNEED          00000000004004e0  000004e0

       0000000000000020  0000000000000000   A       7     1     8

  [10] .rela.dyn         RELA             0000000000400500  00000500

       0000000000000030  0000000000000018   A       6     0     8

  [11] .rela.plt         RELA             0000000000400530  00000530

       0000000000000090  0000000000000018  AI       6    21     8

  [12] .init             PROGBITS         0000000000401000  00001000

       000000000000001b  0000000000000000  AX       0     0     4

  [13] .plt              PROGBITS         0000000000401020  00001020

       0000000000000070  0000000000000010  AX       0     0     16

  [14] .plt.sec          PROGBITS         0000000000401090  00001090

       0000000000000060  0000000000000010  AX       0     0     16

  [15] .text             PROGBITS         00000000004010f0  000010f0

       0000000000000145  0000000000000000  AX       0     0     16

  [16] .fini             PROGBITS         0000000000401238  00001238

       000000000000000d  0000000000000000  AX       0     0     4

  [17] .rodata           PROGBITS         0000000000402000  00002000

       000000000000003b  0000000000000000   A       0     0     8

  [18] .eh_frame         PROGBITS         0000000000402040  00002040

       00000000000000fc  0000000000000000   A       0     0     8

  [19] .dynamic          DYNAMIC          0000000000403e50  00002e50

       00000000000001a0  0000000000000010  WA       7     0     8

  [20] .got              PROGBITS         0000000000403ff0  00002ff0

       0000000000000010  0000000000000008  WA       0     0     8

  [21] .got.plt          PROGBITS         0000000000404000  00003000

       0000000000000048  0000000000000008  WA       0     0     8

  [22] .data             PROGBITS         0000000000404048  00003048

       0000000000000004  0000000000000000  WA       0     0     1

  [23] .comment          PROGBITS         0000000000000000  0000304c

       000000000000002b  0000000000000001  MS       0     0     1

  [24] .symtab           SYMTAB           0000000000000000  00003078

       00000000000004c8  0000000000000018          25    30     8

  [25] .strtab           STRTAB           0000000000000000  00003540

       0000000000000158  0000000000000000           0     0     1

  [26] .shstrtab         STRTAB           0000000000000000  00003698

       00000000000000e1  0000000000000000           0     0     1

(3)程序头

程序头部分是一个结构数组,描述了系统准备程序执行所需的段或其他信息。

程序头:

  Type           Offset             VirtAddr           PhysAddr

                 FileSiz            MemSiz              Flags  Align

  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040

                 0x00000000000002a0 0x00000000000002a0  R      0x8

  INTERP         0x00000000000002e0 0x00000000004002e0 0x00000000004002e0

                 0x000000000000001c 0x000000000000001c  R      0x1

      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]

  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000

                 0x00000000000005c0 0x00000000000005c0  R      0x1000

  LOAD           0x0000000000001000 0x0000000000401000 0x0000000000401000

                 0x0000000000000245 0x0000000000000245  R E    0x1000

  LOAD           0x0000000000002000 0x0000000000402000 0x0000000000402000

                 0x000000000000013c 0x000000000000013c  R      0x1000

  LOAD           0x0000000000002e50 0x0000000000403e50 0x0000000000403e50

                 0x00000000000001fc 0x00000000000001fc  RW     0x1000

  DYNAMIC        0x0000000000002e50 0x0000000000403e50 0x0000000000403e50

                 0x00000000000001a0 0x00000000000001a0  RW     0x8

  NOTE           0x0000000000000300 0x0000000000400300 0x0000000000400300

                 0x0000000000000020 0x0000000000000020  R      0x8

  NOTE           0x0000000000000320 0x0000000000400320 0x0000000000400320

                 0x0000000000000020 0x0000000000000020  R      0x4

  GNU_PROPERTY   0x0000000000000300 0x0000000000400300 0x0000000000400300

                 0x0000000000000020 0x0000000000000020  R      0x8

  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000

                 0x0000000000000000 0x0000000000000000  RW     0x10

  GNU_RELRO      0x0000000000002e50 0x0000000000403e50 0x0000000000403e50

                 0x00000000000001b0 0x00000000000001b0  R      0x1

(4)Dynamic section

Dynamic section at offset 0x2e50 contains 21 entries:

  标记        类型                         名称/

 0x0000000000000001 (NEEDED)             共享库:[libc.so.6]

 0x000000000000000c (INIT)               0x401000

 0x000000000000000d (FINI)               0x401238

 0x0000000000000004 (HASH)               0x400340

 0x000000006ffffef5 (GNU_HASH)           0x400378

 0x0000000000000005 (STRTAB)             0x400470

 0x0000000000000006 (SYMTAB)             0x400398

 0x000000000000000a (STRSZ)              92 (bytes)

 0x000000000000000b (SYMENT)             24 (bytes)

 0x0000000000000015 (DEBUG)              0x0

 0x0000000000000003 (PLTGOT)             0x404000

 0x0000000000000002 (PLTRELSZ)           144 (bytes)

 0x0000000000000014 (PLTREL)             RELA

 0x0000000000000017 (JMPREL)             0x400530

 0x0000000000000007 (RELA)               0x400500

 0x0000000000000008 (RELASZ)             48 (bytes)

 0x0000000000000009 (RELAENT)            24 (bytes)

 0x000000006ffffffe (VERNEED)            0x4004e0

 0x000000006fffffff (VERNEEDNUM)         1

 0x000000006ffffff0 (VERSYM)             0x4004cc

 0x0000000000000000 (NULL)               0x0

(5)Symbol table

       符号表中保存着定位、重定位程序中符号定义和引用的信息,所有重定位需要引用的符号都在其中声明。

Symbol table '.dynsym' contains 9 entries:

   Num:    Value          Size Type    Bind   Vis      Ndx Name

     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND

     1: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND puts@GLIBC_2.2.5 (2)

     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND printf@GLIBC_2.2.5 (2)

     3: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.2.5 (2)

     4: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND getchar@GLIBC_2.2.5 (2)

     5: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__

     6: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND atoi@GLIBC_2.2.5 (2)

     7: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND exit@GLIBC_2.2.5 (2)

     8: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND sleep@GLIBC_2.2.5 (2)

Symbol table '.symtab' contains 51 entries:

   Num:    Value          Size Type    Bind   Vis      Ndx Name

     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND

     1: 00000000004002e0     0 SECTION LOCAL  DEFAULT    1 

     2: 0000000000400300     0 SECTION LOCAL  DEFAULT    2 

     3: 0000000000400320     0 SECTION LOCAL  DEFAULT    3 

     4: 0000000000400340     0 SECTION LOCAL  DEFAULT    4 

     5: 0000000000400378     0 SECTION LOCAL  DEFAULT    5 

     6: 0000000000400398     0 SECTION LOCAL  DEFAULT    6 

     7: 0000000000400470     0 SECTION LOCAL  DEFAULT    7 

     8: 00000000004004cc     0 SECTION LOCAL  DEFAULT    8 

     9: 00000000004004e0     0 SECTION LOCAL  DEFAULT    9 

    10: 0000000000400500     0 SECTION LOCAL  DEFAULT   10 

    11: 0000000000400530     0 SECTION LOCAL  DEFAULT   11 

    12: 0000000000401000     0 SECTION LOCAL  DEFAULT   12 

    13: 0000000000401020     0 SECTION LOCAL  DEFAULT   13 

    14: 0000000000401090     0 SECTION LOCAL  DEFAULT   14 

    15: 00000000004010f0     0 SECTION LOCAL  DEFAULT   15 

    16: 0000000000401238     0 SECTION LOCAL  DEFAULT   16 

    17: 0000000000402000     0 SECTION LOCAL  DEFAULT   17 

    18: 0000000000402040     0 SECTION LOCAL  DEFAULT   18 

    19: 0000000000403e50     0 SECTION LOCAL  DEFAULT   19 

    20: 0000000000403ff0     0 SECTION LOCAL  DEFAULT   20 

    21: 0000000000404000     0 SECTION LOCAL  DEFAULT   21 

    22: 0000000000404048     0 SECTION LOCAL  DEFAULT   22 

    23: 0000000000000000     0 SECTION LOCAL  DEFAULT   23 

    24: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS hello.c

    25: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS

    26: 0000000000403e50     0 NOTYPE  LOCAL  DEFAULT   19 __init_array_end

    27: 0000000000403e50     0 OBJECT  LOCAL  DEFAULT   19 _DYNAMIC

    28: 0000000000403e50     0 NOTYPE  LOCAL  DEFAULT   19 __init_array_start

    29: 0000000000404000     0 OBJECT  LOCAL  DEFAULT   21 _GLOBAL_OFFSET_TABLE_

    30: 0000000000401230     5 FUNC    GLOBAL DEFAULT   15 __libc_csu_fini

    31: 0000000000404048     0 NOTYPE  WEAK   DEFAULT   22 data_start

    32: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND puts@@GLIBC_2.2.5

    33: 000000000040404c     0 NOTYPE  GLOBAL DEFAULT   22 _edata

    34: 0000000000401238     0 FUNC    GLOBAL HIDDEN    16 _fini

    35: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND printf@@GLIBC_2.2.5

    36: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@@GLIBC_

    37: 0000000000404048     0 NOTYPE  GLOBAL DEFAULT   22 __data_start

    38: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND getchar@@GLIBC_2.2.5

    39: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__

    40: 0000000000402000     4 OBJECT  GLOBAL DEFAULT   17 _IO_stdin_used

    41: 00000000004011c0   101 FUNC    GLOBAL DEFAULT   15 __libc_csu_init

    42: 0000000000404050     0 NOTYPE  GLOBAL DEFAULT   22 _end

    43: 0000000000401120     5 FUNC    GLOBAL HIDDEN    15 _dl_relocate_static_pie

    44: 00000000004010f0    47 FUNC    GLOBAL DEFAULT   15 _start

    45: 000000000040404c     0 NOTYPE  GLOBAL DEFAULT   22 __bss_start

    46: 0000000000401125   146 FUNC    GLOBAL DEFAULT   15 main

    47: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND atoi@@GLIBC_2.2.5

    48: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND exit@@GLIBC_2.2.5

    49: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND sleep@@GLIBC_2.2.5

    50: 0000000000401000     0 FUNC    GLOBAL HIDDEN    12 _init

5.4 hello的虚拟地址空间

       打开edb,通过 data dump 查看加载到虚拟地址的程序代码。查看 ELF 格式文件中的 Program Headers,它告诉链接器运行时加载的内容,并提供动态链接的信息。每一个表项提供了各段在虚拟地址空间和物理地址空间的各方面的信息。在下面可以看出,程序包含PHDR,INTERP,LOAD ,DYNAMIC,NOTE ,GNU_STACK,GNU_RELRO几个部分,如下图所示:

图 18 edb中Data Dump视图

       其中PHDR 保存程序头表。INTERP 指定在程序已经从可执行文件映射到内存之后,必须调用的解释器。LOAD 表示一个需要从二进制文件映射到虚拟地址空间的段。其中保存了常量数据、程序的目标代码等。DYNAMIC 保存了由动态链接器使用的信息。NOTE 保存辅助信息。GNU_STACK:权限标志,用于标志栈是否是可执行。GNU_RELRO:指定在重定位结束之后哪些内存区域是需要设置只读。

 图 19  Linux 进程的虚拟地址空间

5.5 链接的重定位过程分析

在Shell中使用命令objdump -d -r hello > hello1.asm生成反汇编文件hello1.asm

 图 20 生成.asm文件

       与第四章中生成的hello.asm文件进行比较,其不同之处如下:

① 链接后函数数量增加

       链接后的反汇编文件hello2.asm中,多出了.plt,puts@plt,printf@plt,getchar@plt,exit@plt,sleep@plt等函数的代码。这是因为动态链接器将共享库中hello.c用到的函数加入可执行文件中。

 图 21 链接后的函数

② 函数调用指令call的参数发生变化

       在链接过程中,链接器解析了重定位条目,call之后的字节代码被链接器直接修改为目标地址与下一条指令的地址之差,指向相应的代码段,从而得到完整的反汇编代码。

 图 22  call指令的参数

③ 跳转指令参数发生变化

       在链接过程中,链接器解析了重定位条目,并计算相对距离,修改了对应位置的字节代码为PLT 中相应函数与下条指令的相对地址,从而得到完整的反汇编代码。

 图 23 跳转指令的参数

5.6 hello的执行流程

地址(后6位)和子函数名

401000 <_init>

401020 <.plt>

401030 <puts@plt>

401040 <printf@plt>

401050 <getchar@plt>

401060 <atoi@plt>

401070 <exit@plt>

401080 <sleep@plt>

401090 <_start>

4010c0 <_dl_relocate_static_pie>

4010c1 <main>

401150 <__libc_csu_init>

4011b0 <__libc_csu_fini>

4011b4 <_fini>

通过edb的调试,一步一步地记录下call命令进入的函数。

 图 24  edb运行程序

5.7 Hello的动态链接分析

在elf文件中找到:

[20] .got              PROGBITS         0000000000403ff0  00002ff0

       0000000000000010  0000000000000008  WA       0     0     8

[21] .got.plt          PROGBITS         0000000000404000  00003000

       0000000000000048  0000000000000008  WA       0     0     8

进入edb查看:

 图 25  edb执行init之前的地址

 图 26  edb执行init之后的地址

        对于变量而言,利用代码段和数据段的相对位置不变的原则去计算正确地址。

        对于库函数而言,需要plt、got合作。plt初始存的是一批代码,它们跳转到got所指示的位置,然后调用链接器。初始时got里面存的都是plt的第二条指令,随后链接器修改got,下一次再调用plt时,指向的就是正确的内存地址。接下来执行程序的过程中,就可以使用过程链接表plt和全局偏移量表got进行动态链接。

5.8 本章小结

本章中介绍了链接的概念与作用。通过查看hello的虚拟地址空间,得到了链接后的hello可执行文件的ELF格式文本hello1.elf,据此分析了hello1.elf与hello.elf的异同,更好地掌握了链接与之中重定位的过程。之后,根据反汇编文件hello1.asm与hello.asm的比较,加深了对重定位与动态链接的理解。


6hello进程管理

6.1 进程的概念与作用

(1)进程的概念

       狭义定义:进程是正在运行的程序的实例。

       广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。

(2)进程的作用

       每次运行程序时,shell创建一新进程,在这个进程的上下文切换中运行这个可执行目标文件。应用程序也能够创建新进程,并且在新进程的上下文中运行它们自己的代码或其他应用程序。

       进程提供给应用程序的关键抽象:一个独立的逻辑控制流,如同程序独占处理器;一个私有的地址空间,如同程序独占内存系统。

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

(1)Shell的作用

       Shell 是一个用C语言编写的交互型应用程序,代表用户运行其他程序。Shell 应用程序提供了一个界面,用户可以通过这个界面进行系统的基本操作,访问操作系统内核的服务。

(2)Shell的处理流程

Shell的处理流程大致如下:

① 从Shell终端读入输入的命令。

② 切分输入字符串,获得并识别所有的参数

③ 若输入参数为内置命令,则立即执行

④ 若输入参数并非内置命令,则调用相应的程序为其分配子进程并运行

⑤ 若输入参数非法,则返回错误信息

⑥ 处理完当前参数后继续处理下一参数,直到处理完毕

6.3 Hello的fork进程创建过程

       根据shell的处理流程,可以推断,输入命令执行hello后,父进程如果判断不是内部指令,即会通过fork函数创建子进程。子进程获取了与父进程的上下文,包括栈、通用寄存器、程序计数器,环境变量和打开的文件相同的一份副本。子进程与父进程的最大区别是有着跟父进程不一样的PID。Fork函数只会被调用一次,但会返回两次,在父进程中,fork返回子进程的PID,在子进程中,fork返回0。子进程可以读取父进程打开的任何文件。当子进程运行结束时,父进程如果仍然存在,则执行对子进程的回收,否则就由init进程回收子进程。

6.4 Hello的execve过程

       execve函数在加载并运行可执行目标文件Hello,且带列表argv和环境变量列表envp。该函数的作用就是在当前进程的上下文中加载并运行一个新的程序。

       只有当出现错误时,例如找不到Hello时,execve才会返回到调用程序,这里与一次调用两次返回的fork不同。

       在execve加载了Hello之后,它调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数,该主函数有如下的原型:

int main(intargc , char **argv , char *envp);

       结合虚拟内存和内存映射过程,可以更详细地说明exceve函数实际上是如何加载和执行程序Hello:

① 删除已存在的用户区域(自父进程独立)。

② 映射私有区:为Hello的代码、数据、.bss和栈区域创建新的区域结构,所有这些区域都是私有的、写时才复制的。

③ 映射共享区:比如Hello程序与标准C库libc.so链接,这些对象都是动态链接到Hello的,然后再用户虚拟地址空间中的共享区域内。

④ 设置PC:exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。

6.5 Hello的进程执行

(1)相关概念

① 逻辑控制流

系列程序计数器 PC 的值的序列叫做逻辑控制流。由于进程是轮流使用处理器的,同一个处理器每个进程执行它的流的一部分后被抢占,然后轮到其他进程。

② 用户模式和内核模式

处理器使用一个寄存器提供两种模式的区分。用户模式的进程不允许执行特殊指令,不允许直接引用地址空间中内核区的代码和数据;内核模式进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。

③ 上下文

上下文就是内核重新启动一个被抢占的进程所需要恢复的原来的状态,由寄存器、程序计数器、用户栈、内核栈和内核数据结构等对象的值构成。

(2)进程调度的过程

       在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行,我们说内核调度了这个进程。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。

       以执行sleep函数为例,sleep函数请求调用休眠进程,sleep将内核抢占,进入倒计时,当倒计时结束后,hello程序重新抢占内核,继续执行。

 图 27  进程上下文切换

       初始时,控制流在hello内,处于用户模式

       调用系统函数sleep后,进入内核态,此时间片停止。

       2s后,发送中断信号,转回用户模式,继续执行指令。

(3)用户态与核心态转换

       为了能让处理器安全运行,不至于损坏操作系统,必然需要先知应用程序可执行指令所能访问的地址空间范围。因此,就存在了用户态与核心态的划分,核心态可以说是“创造模式”,拥有最高的访问权限,处理器以一个寄存器当做模式位来描述当前进程的特权。进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他情况下始终处于用户权限之中,保证了系统的安全性。

6.6 hello的异常与信号处理

异常类型:

类别

原因

异步/同步

返回行为

中断

来自I/O设备的信号

异步

总是返回到下一条指令

陷阱

有意的异常

同步

总是返回到下一条指令

故障

潜在可恢复的错误

同步

可能返回到当前指令

终止

不可恢复的错误

同步

不会返回

表格 7 异常类型

处理方式:

 图 28 中断处理方式

 图 29 陷阱处理方式

 图 30 故障处理方式

 图 31 终止处理方式

打开Shell,输入命令./hello 120L022408 高瑞祥 1,带参数执行生成的可执行文件。

(1)正常运行状态

在程序正常运行时,打印8次提示信息,以输入回车为标志结束程序,并回收进程。

 图 32 程序正常执行

(2)运行时按回车

在程序运行时按回车,会多打印几处空行,程序可以正常结束。

图 33 运行时按回车

(3)运行时按下Ctrl + C

按下Ctrl + C,Shell进程收到SIGINT信号,Shell结束并回收hello进程。

 图 34 运行时按下Ctrl + C

(4)运行时按下Ctrl + Z

按下Ctrl + Z,Shell进程收到SIGSTP信号,Shell显示屏幕提示信息并挂起hello进程。

 图 35 运行时按下Ctrl + Z

对hello进程的挂起可由ps和jobs命令查看,可以发现hello进程确实被挂起而非被回收,且其job代号为1。

 图 36 用ps和jobs命令查看挂起进程

在Shell中输入pstree命令,可以将所有进程以树状图显示(此处仅展示部分):

 图 37 用pstree命令查看所有进程

输入kill命令,则可以杀死指定(进程组的)进程:

 图 38 用kill命令杀死指定进程

       输入fg 1则命令将hello进程再次调到前台执行,可以发现Shell首先打印hello的命令行命令,hello再从挂起处继续运行,打印剩下的语句。程序仍然可以正常结束,并完成进程回收。

 图 39 用fg命令将进程调回前台

(5)不停乱按

       在程序执行过程中乱按所造成的输入均缓存到stdin,当getchar的时候读出一个’\n’结尾的字串(作为一次输入),hello结束后,stdin中的其他字串会当做Shell的命令行输入。

 图 40 不停乱按的情况

6.7本章小结

       本章介绍了进程的概念与作用,以及Shell-bash的基本概念。针对进程,在这一章中根据hello可执行文件的具体示例研究了fork, execve函数的原理与执行过程。在hello运行过程中,内核有选择对其进行管理,决定何时进行上下文切换。并且当接受到不同的异常信号时,异常处理程序将对异常信号做出相应,执行相应的代码,每种信号都有不同的处理机制,对不同的异常信号,hello也有不同的处理结果。


7hello的存储管理

7.1 hello的存储器地址空间

(1)逻辑地址

       逻辑地址(Logical Address)是指由程序hello产生的与段相关的偏移地址部分(hello.o)。

(2)线性地址

       线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。程序hello的代码会产生逻辑地址,或者说是(即hello程序)段中的偏移地址,它加上相应段的基地址就生成了一个线性地址。

(3)虚拟地址

       有时我们也把逻辑地址称为虚拟地址。因为与虚拟内存空间的概念类似,逻辑地址也是与实际物理内存容量无关的,是hello中的虚拟地址。

(4)物理地址

       物理地址(Physical Address)是指出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。如果启用了分页机制,那么hello的线性地址会使用页目录和页表中的项变换成hello的物理地址;如果没有启用分页机制,那么hello的线性地址就直接成为物理地址了。

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

       Intel处理器从逻辑地址到线性地址的变换通过段式管理的方式实现。每个程序在系统中都保存着一个段表,段表保存着该程序各段装入主存的状况信息,包括段号或段名、段起点、装入位、段的长度、主存占用区域表、主存可用区域表等,从而方便进行段式管理。

       在段寄存器中,存放着段选择符,可以通过段选择符来得到对应段首地址。段选择符的结构如下:

 图 41 段选择符的情况

       其包含三部分:索引,TI,RPL

       索引:用来确定当前使用的段描述符在描述符表中的位置;

       TI:根据TI的值判断选择全局描述符表(TI=0,GDT)或选择局部描述符表(TI=1,LDT);

       RPL:判断重要等级。RPL=00,为第0级,位于最高级的内核,RPL=11,为第3级,位于最低级的用户状态;

       通过一个索引,可以定位到段描述符,进而通过段描述符得到段基址。段基址与偏移量结合就得到了线性地址,虚拟地址。

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

       线性地址(VA)到物理地址(PA)之间的转换通过对虚拟地址内存空间进行分页的分页机制完成。

       通过7.2节中的段式管理过程,可以得到了线性地址/虚拟地址,记为VA。虚拟地址可被分为两个部分:VPN(虚拟页号)和VPO(虚拟页偏移量),根据计算机系统的特性可以确定VPN与VPO的具体位数,由于虚拟内存与物理内存的页大小相同,因此VPO与PPO(物理页偏移量)一致。而PPN(物理页号)则需通过访问页表中的页表条目(PTE)获取,如下图所示。

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

       若PTE的有效位为1,则发生页命中,可以直接获取到物理页号PPN,PPN与PPO共同组成物理地址。

       若PTE的有效位为0,说明对应虚拟页没有缓存到物理内存中,产生缺页故障,调用操作系统的内核的缺页处理程序,确定牺牲页,并调入新的页面。再返回到原来的进程,再次调用导致缺页的指令。此时发生页命中,获取到PPN,与PPO共同组成物理地址。

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

       针对Intel Core i7 CPU研究VA到PA的变换。

       Intel Core i7 CPU的基本参数如下:

  1. 虚拟地址空间48位(n=48)
  2. 物理地址空间52位(m=52)
  3. TLB四路十六组相连
  4. L1,L2,L3块大小为64字节
  5. L1,L2八路组相连
  6. L3十六路组相连
  7. 页表大小4KB(P=4x1024=2^12),四级页表,页表条目(PTE)大小8字节

       由上述信息可以得知,VPO与PPO有p=12位,故VPN为36位,PPN为40位。单个页表大小4KB,PTE大小8字节,则单个页表有512个页表条目,需要9位二进制进行索引,而四级页表则需要36位二进制进行索引,对应着36位的VPN。TLB有16组,故TLBI有t=4位,TLBT有36-4=32位。

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

       如图所示, CPU产生虚拟地址VA,并将其传送至MMU,MMU使用前36位VPN作为TLBT(前32位)+TLBI(后4位)在TLB中进行匹配,若命中,则得到PPN(40bit)与VPO(12bit)组合成物理地址PA(52bit)。若TLB没有命中,则MMU向页表中查询,由CR3确定第一级页表的起始地址,VPN1(9bit)确定在第一级页表中的偏移量,查询出PTE,如果在物理内存中且权限符合,则执行下一步确定第二级页表的起始地址,以此类推,最终在第四级页表中查询到PPN,与VPO组合成PA,并向TLB中添加条目。多级页表的工作原理展示如下:

图 44 多级页表的工作原理

       若查询PTE的时候发现不在物理内存中,则引发缺页故障。如果发现权限不够,则引发段错误。

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

       因为三级Cache的工作原理基本相同,所以在这里以L1 Cache为例,介绍三级Cache支持下的物理内存访问。

       L1 Cache的基本参数如下:

  1. 8路64组相连
  2. 块大小64字节

       由L1 Cache的基本参数,可以分析知:

       块大小64字节→需要6位二进制索引→块偏移6位

       共64组→需要6位二进制索引→组索引6位

       余下标记位→需要PPN+PPO-6-6=40位

       故L1 Cache可被划分如下(从左到右):

       CT(40bit)CI(6bit)CO(6bit)

       在7.4中我们已经由虚拟地址VA转换得到了物理地址PA,首先使用CI进行组索引,每组8路,对8路的块分别匹配CT(前40位)如果匹配成功且块的valid标志位为1,则命中(hit),根据数据偏移量CO取出相应的数据后返回。

       若没有匹配成功或者匹配成功但是标志位是1,则不命中(miss),向下一级缓存中请求数据(请求顺序为L2 Cache→L3 Cache→主存,若仍不命中才继续向下一级请求)。查询到数据之后,需要对数据进行读入,一种简单的放置策略如下:若映射到的组内有空闲块,则直接放置在空闲块中,若当前组内没有空闲块,则产生冲突(evict),采用LFU策略进行替换。

 图 45 3级Cache

7.6 hello进程fork时的内存映射

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

       当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当着两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。

7.7 hello进程execve时的内存映射

execve函数加载并运行hello需要以下几个步骤:

1、删除已存在的用户区域

      删除当前进程hello虚拟地址的用户部分中的已存在的区域结构。

2、映射私有区域

       为新程序的代码、数据、bss和栈区域创建新的私有的、写时复制的区域结构。其中,代码和数据区域被映射为hello文件中的.text和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。

3、映射共享区域

       若hello程序与共享对象或目标(如标准C库libc.so)链接,则将这些对象动态链接到hello程序,然后再映射到用户虚拟地址空间中的共享区域内。

4、设置程序计数器

       最后,execve设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。

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

       发生一个缺页异常后,控制会转移到内核的缺页处理程序。判断虚拟地址是否合法,若不合法,则产生一个段错误,然后终止这个进程。

       若操作合法,则缺页处理程序从物理内存中确定一个牺牲页,若该牺牲页被修改过,则将它换出到磁盘,换入新的页面并更新页表。当缺页处理程序返回时,CPU 再次执行引起缺页的指令,将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面现在缓存在物理内存中,所以就会命中,主存将所请求字返回给处理器。

 图 46 缺页中断处理

7.9动态存储分配管理

       动态内存管理的基本方法与策略介绍如下:

       动态内存分配器维护着一个称为堆的进程的虚拟内存区域。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放可以由应用程序显式执行或内存分配器自身隐式执行。

       具体而言,分配器分为两种基本风格:显式分配器、隐式分配器。

       显式分配器:要求应用显式地释放任何已分配的块。

       隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。

下面介绍动态存储分配管理中较为重要的概念:

1、隐式链表

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

       对于隐式链表,其结构如下:

 图 47 隐式链表的结构

2、显式链表

       在每个空闲块中,都包含一个前驱(pred)与后继(succ)指针,从而减少了搜索与适配的时间。

       显式链表的结构如下:

 图 48 显式链表的结构

3、带边界标记的合并

       采取使用边界标记的堆块的格式,在堆块的末尾为其添加一个脚部,其为头部的副本。添加脚部之后,分配器就可以通过检查前面一个块的脚部,判断前面一个块的起始位置和状态。从而实现快速合并,减小性能消耗。

4、分离存储

       维护多个空闲链表,其中,每个链表的块具有相同的大小。将所有可能的块大小分成一些等价类,从而进行分离存储。

7.10本章小结

       本章主要介绍了hello 的存储器地址空间、intel 的段式管理、hello 的页式管理, VA 到PA 的变换、物理内存访问,hello进程fork、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理的相关内容,对hello的存储管理有了较为深入的讲解。


8hello的IO管理

8.1 Linux的IO设备管理方法

       设备的模型化:文件

       设备管理:unix io接口

       所有的IO设备都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行。这种将设备映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。这使得所有的输入和输出都能以一种统一且一致的方式来执行:打开文件、改变当前的文件位置、读写文件、关闭文件。

8.2 简述Unix IO接口及其函数

(1)Unix I/O接口

1、打开文件

       一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。对于Shell创建的每个进程,其都有三个打开的文件:标准输入,标准输出,标准错误。

2、改变当前的文件位置

        对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。

3、读写文件

       一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。

4、关闭文件

       内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中去。

(2)Unix I/O函数

1. open()函数

功能描述:用于打开或创建文件,在打开或创建文件时可以指定文件的属性及用户的权限等各种参数。

函数原型:int open(const char *pathname,int flags,int perms)

参数:pathname:被打开的文件名(可包括路径名如"dev/ttyS0")flags:文件打开方式,

返回值:成功:返回文件描述符;失败:返回-1

2. close()函数

功能描述:用于关闭一个被打开的的文件

所需头文件: #include <unistd.h>

函数原型:int close(int fd)

参数:fd文件描述符

函数返回值:0成功,-1出错

3. read()函数

功能描述: 从文件读取数据。

所需头文件: #include <unistd.h>

函数原型:ssize_t read(int fd, void *buf, size_t count);

参数:fd:将要读取数据的文件描述词。buf:指缓冲区,即读取的数据会被放到这个缓冲区中去。count: 表示调用一次read操作,应该读多少数量的字符。

返回值:返回所读取的字节数;0(读到EOF);-1(出错)

4. write()函数

功能描述: 向文件写入数据。

所需头文件: #include <unistd.h>

函数原型:ssize_t write(int fd, void *buf, size_t count);

返回值:写入文件的字节数(成功);-1(出错)

5. lseek()函数

功能描述: 用于在指定的文件描述符中将将文件指针定位到相应位置。

所需头文件:#include <unistd.h>,#include <sys/types.h>

函数原型:off_t lseek(int fd, off_t offset,int whence);

参数:fd;文件描述符。offset:偏移量,每一个读写操作所需要移动的距离,单位是字节,可正可负(向前移,向后移)

返回值:成功:返回当前位移;失败:返回-1

8.3 printf的实现分析

       查看windows系统下的printf函数体:

 图 49 printf的函数体

       形参列表中的…是可变形参的一种写法,当传递参数的个数不确定时,用这种方式来表示。

       va_list的定义:typedef char *va_list,说明它是一个字符指针,其中 (char*)(&fmt) + 4) 即arg表示的是...中的第一个参数。

       再进一步查看windows系统下的vsprintf函数体:

 图 50 vsprintf的函数体

       则知道vsprintf程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。

       在printf中调用系统函数write(buf,i)将长度为i的buf输出。write函数如下:

       printf函数的功能为接受一个格式化命令,并按指定的匹配的参数格式化输出,故i = vsprintf(buf, fmt, arg)是得到打印出来的字符串长度,其后的write(buf, i)是将buf中的i个元素写到终端。

       因此,vsprintf的作用为接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,进而产生格式化输出。

       再进一步对write进行追踪:

 图 51 write的情况

       这里给几个寄存器传递了参数,然后以一个int INT_VECTOR_SYS_CALL结束。INT_VECTOR_SYS_CALL代表通过系统调用syscall,查看syscall的实现:

 图 52 syscall的情况

       syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码,符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

       getchar有一个int型的返回值。当程序调用getchar时,程序就等着用户按键,用户输入的字符被存放在键盘缓冲区中直到用户按回车为止(回车字符也放在缓冲区中)。

       当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的第一个字符的ascii码,如出错返回-1,且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为后,才等待用户按键。

       异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。

       getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

8.5本章小结

       本章主要介绍了 Linux 的 I/O 设备的基本概念和管理方法,以及Unix I/O 接口及其函数,最后通过对printf函数和getchar函数的底层实现的分析,对其工作过程有了基本了解。

结论

(1)总结hello所经历的过程

① 预处理

       将hello.c中include的所有外部的头文件头文件内容直接插入程序文本中,完成字符串的替换,方便后续处理。

② 编译

       通过词法分析和语法分析,将合法指令翻译成等价汇编代码。通过编译过程,编译器将hello.i 翻译成汇编语言文件 hello.s。

③ 汇编

       将hello.s汇编程序翻译成机器语言指令,并把这些指令打包成可重定位目标程序格式,最终结果保存在hello.o 目标文件中。

④ 链接

       通过链接器,将hello的程序编码与动态链接库等收集整理成为一个单一文件,生成完全链接的可执行的目标文件hello。

⑤ 加载运行

       打开Shell,在其中键入 ./hello 120L022408 高瑞祥 1,终端为其fork新建进程,并通过execve把代码和数据加载入虚拟内存空间,程序开始执行。

⑥ 执行指令

       在该进程被调度时,CPU为hello其分配时间片,在一个时间片中,hello享有CPU全部资源,PC寄存器一步一步地更新,CPU不断地取指,顺序执行自己的控制逻辑流。

⑦ 访存

       内存管理单元MMU将逻辑地址,一步步映射成物理地址,进而通过三级高速缓存系统访问物理内存/磁盘中的数据。

⑧ 动态申请内存

       printf 会调用malloc 向动态内存分配器申请堆中的内存。

⑨ 信号处理

       进程时刻等待着信号,如果运行途中键入ctr-c ctr-z 则调用shell 的信号处理函数分别进行停止、挂起等操作,对于其他信号也有相应的操作。

⑩终止并被回收

       Shell父进程等待并回收hello子进程,内核删除为hello进程创建的所有数据结构。

(2)感悟

       《深入理解计算机系统》一书深入浅出地介绍了计算机系统的基本概念,包括最底层的内存中的数据表示、流水线指令的构成、虚拟存储器、编译系统、动态加载库,以及用户应用等。书中提供了大量实际操作,可以帮助读者更好地理解程序执行的方式,改进程序的执行效率。此书以程序员的视角全面讲解了计算机系统,深入浅出地介绍了处理器、编译器、操作系统和网络环境。

       对于我们哈工大计算机的同学来说,这学期学习的计算机系统课程使我们受益匪浅。这门课程让我认识到,我应该成为优秀的程序员、工程师,而不是进行重复劳动的工具人或码农。我们不应该只盯着顶层的实现,而忽视底层的逻辑。对于计算机底层架构的了解同样十分重要!


附件

文件名

功能

hello.c

源程序

hello.i

预处理后得到的文本文件

hello.s

编译后得到的汇编语言文件

hello.o

汇编后得到的可重定位目标文件

hello.elf

用readelf读取hello.o得到的ELF格式信息

hello.asm

反汇编hello.o得到的反汇编文件

hello1.elf

由hello可执行文件生成的.elf文件

hello1.asm

反汇编hello可执行文件得到的反汇编文件

hello

可执行文件


参考文献

[1] Randal E.Bryant, David O'Hallaron. 深入理解计算机系统[M]. 机械工业出版社.2018.4 

[2] Pianistx.printf 函数实现的深入剖析[EB/OL].2013[2021-6-9].

[转]printf 函数实现的深入剖析 - Pianistx - 博客园

[3] 梦想之家xiao_chen.ELF文件头更详细结构[EB/OL].2017[2021-6-10].

https://blog.csdn.net/qq_32014215/article/details/76618649.

[4] Florian.printf背后的故事[EB/OL].2014[2021-6-10].

https://www.cnblogs.com/fanzhidongyzby/p/3519838.html.

[5]  CSDN博客 Ubuntu系统预处理、编译、汇编、链接指令

[6]  博客园 从汇编层面看函数调用的实现原理

[7]  CSDN博客 ELF可重定位目标文件格式

[8]  博客园 shell命令执行过程

[9]  《步步惊芯——软核处理器内部设计分析》 TLB的作用及工作过程

posted @ 2022-05-19 18:35  何以牵尘  阅读(400)  评论(0编辑  收藏  举报