120L020820-庞博-HIT-ICS2022大作业报告

计算机系统

大作业

题     目  程序人生-Hello’s P2P 

专       业  计算机类               

学     号  120L020820             

班     级  2003009              

学       生  庞博                  

指 导 教 师  郑贵滨                  

计算机科学与技术学院

2021年5月

摘  要

本论文将CSAPP课程所学内容通过hello小程序的一生,对我们所学进行全面的梳理与回顾。我们主要在Ubuntu下进行相关操作,合理运用了Ubuntu下的操作工具,进行细致的历程分析,目的是加深对计算机系统的理解。

关键词:hello;程序的一生;计算机系统;程序的编译执行;进程;存储管理

(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分

目  录

第1章 概述... - 5 -

1.1 Hello简介... - 5 -

1.2 环境与工具... - 5 -

1.3 中间结果... - 5 -

1.4 本章小结... - 6 -

第2章 预处理... - 7 -

2.1 预处理的概念与作用... - 7 -

2.2在Ubuntu下预处理的命令... - 7 -

2.3 Hello的预处理结果解析... - 7 -

2.4 本章小结... - 10 -

第3章 编译... - 11 -

3.1 编译的概念与作用... - 11 -

3.2 在Ubuntu下编译的命令... - 11 -

3.3 Hello的编译结果解析... - 11 -

3.3.1 汇编代码基本信息... - 12 -

3.3.2 数据和赋值... - 12 -

3.3.3 算数操作... - 13 -

3.3.4 条件操作和控制转移... - 13 -

3.3.5 数组/指针/结构操作... - 13 -

3.3.6 函数操作... - 14 -

3.4 本章小结... - 16 -

第4章 汇编... - 18 -

4.1 汇编的概念与作用... - 18 -

4.2 在Ubuntu下汇编的命令... - 18 -

4.3 可重定位目标elf格式... - 18 -

4.3.1命令:readelf -a hello.o > ./helloelf.txt - 18 -

4.3.2 ELF头:... - 18 -

4.3.3 节头目表... - 19 -

4.3.4 重定位节:... - 20 -

4.3.5 符号表:... - 21 -

4.4 Hello.o的结果解析... - 21 -

4.5 本章小结... - 23 -

第5章 链接... - 24 -

5.1 链接的概念与作用... - 24 -

5.2 在Ubuntu下链接的命令... - 24 -

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

5.4 hello的虚拟地址空间... - 28 -

5.5 链接的重定位过程分析... - 29 -

5.6 hello的执行流程... - 30 -

5.7 Hello的动态链接分析... - 30 -

5.8 本章小结... - 31 -

第6章 hello进程管理... - 32 -

6.1 进程的概念与作用... - 32 -

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

6.3 Hello的fork进程创建过程... - 32 -

6.4 Hello的execve过程... - 33 -

6.5 Hello的进程执行... - 33 -

6.6 hello的异常与信号处理... - 34 -

6.7本章小结... - 37 -

第7章 hello的存储管理... - 39 -

7.1 hello的存储器地址空间... - 39 -

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

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

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

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

7.6 hello进程fork时的内存映射... - 42 -

7.7 hello进程execve时的内存映射... - 42 -

7.8 缺页故障与缺页中断处理... - 42 -

7.9动态存储分配管理... - 43 -

7.10本章小结... - 44 -

第8章 hello的IO管理... - 45 -

8.1 Linux的IO设备管理方法... - 45 -

8.2 简述Unix IO接口及其函数... - 45 -

8.3 printf的实现分析... - 46 -

8.4 getchar的实现分析... - 47 -

8.5本章小结... - 48 -

结论... - 48 -

附件... - 50 -

参考文献... - 51 -

第1章 概述

1.1 Hello简介

P2P

(1)程序Program: 在文本编辑器或IDE中键入代码得到 hello.c 程序,此时获得了一个静态的程序文件,保存在硬盘中。在 Linux 操作系统中,hello.c文件经过预处理器 cpp 的预处理、 编译器ccl 的编译、 汇编器as 的汇编、 链接器ld 的链接最终成为可执目标程序 hello,也是以静态形式保存在硬盘。

(2)进程Process:在 shell 中键入启动命令后, shell命令行解释器为其构造命令行参数向量argv和环境参数向量envp,shell父进程调用fork函数创建一个子进程,此时内存地址空间与shell进程完全相同,包括代码段,数据段,堆栈区等。子进程中调用execve函数,在当前进程的上下文中加载并运行hello程序。设置PC指向hello程序的第一行代码处,hello程序开始在一个进程的上下文中运行。

020:

shell 为 hello 进程 execve,映射虚拟内存,设置PC指向hello程序的第一条指令。进入程序入口后程序开始载入物理内存。进入 main 函数执行目标代码, CPU 为运行的 hello 分配时间片执行逻辑控制流。当程序运行结束后,shell父进程负责回收僵死的hello进程,内核删除相关数据结构。

1.2 环境与工具

CPU: AMD Ryzen 5 4600H with Radeon Graphics  3.00 GHz

内存:16.0GB

磁盘:476.92GB

软件环境:Windows10 64位;Ubuntu20.04  Ubuntu

工具:gcc,edb,gdb,objdump,Visual Studio

1.3 中间结果

文件的作用

文件名

预处理后的文件

hello.i

编译之后的汇编文件

hello.s

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

hello.o

链接之后的可执行目标文件

hello

hello.o 的 ELF 格式

helloelf.txt

hello.o 的反汇编代码

hellodump.s

hello 的反汇编代码

dump.s

Hello的ELF格式

elf.txt

1.4 本章小结

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

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

预处理的概念:

删除”#define”并展开所定义的宏;处理所有条件预编译指令,如“#if”,“#ifdef”,“#endif”等;插入头文件到”#include”处,可以递归方式进行处理;删除所有的注释”//”和”/* */”;添加行号和文件名标识,以便编译时编译器产生调试用的行号信息;保留所有的#pragma编译指令

预处理的作用:

  1. 将源文件中用#include 形式声明的文件复制到新的程序中。比如 hello.c 中的#include<stdio.h> 等命令告诉预处理器读取系统头文件 stdio.h 的内容,并把它直接插入到程序文本中。
  2. 用实际值替换用#define 定义的字符串
  3. 根据#if 后面的条件决定需要编译的代码
  4. 特殊符号,预编译程序可以识别一些特殊的符号, 预编译程序对于在源程序中出现的这些串将用合适的值进行替换。

2.2在Ubuntu下预处理的命令

      命令:cpp hello.c > hello.i 或gcc -E hello.c -o hello.i

                                                图 1 预处理命令

2.3 Hello的预处理结果解析

可以发现整个程序已经拓展为3060行,原来hello.c的程序出现在3046行及之后。在这之前出现的是头文件 stdio.h,unistd.h,stdlib.h 的依次展开。 经过预处理之后,hello.c转化为hello.i文件,打开该文件可以发现,文件的内容增加,且仍为可以阅读的C语言程序文本文件。对原文件中的宏进行了宏展开,头文件中的内容被包含进该文件中。例如声明函数、定义结构体、定义变量、定义宏等内容。另外,如果代码中有#define命令还会对相应的符号进行替换。

插入库所在位置:

图 2 hello.i库文件部分

库中预置函数:

图 3 hello.i声明函数部分

图 4 hello.i声明函数部分

源代码位置:

图 5 hello.i源代码部分

2.4 本章小结

本章主要介绍了预处理(包括头文件的展开、宏替换、去掉注释、条件编译)的概念和应用功能,以及Ubuntu下预处理的两个指令,同时具体到我们的hello.c文件的预处理结果hello.i文本文件解析,详细了解了预处理的内涵。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

编译的概念:

编译的过程就是将预处理之后得到的预处理文件hello.i进行词法分析,语法分析,语义分析,优化后,生成汇编代码文件。用来进行编译处理的程序称为编译程序或编译器(Complier)。

编译的作用:

编译包括以下基本流程:

  1. 执行语法分析:编译程序的语法分析器以单词符号作为输入,分析单词符号串是否形成符合语法规则的语法单位。
  2. 执行代码优化:指对程序进行多种等价变换,在保证程序执行功能不变的情况下,使得从变换后的程序出发,能生成更有效的目标代码。
  3. 生成目标代码:生成目标代码是编译的最后一个阶段。目标代码生成器把语法分析后或优化后的中间代码变换成目标代码,即汇编语言代码。

3.2 在Ubuntu下编译的命令

命令:gcc -S hello.i -o hello.s

图 6 编译命令

3.3 Hello的编译结果解析

3.3.1 汇编代码基本信息

图7 汇编代码基本信息

.file:声明源文件

.text:代码节

.section:

.rodata:只读代码段

.align:数据或者指令的地址对其方式

.string:声明一个字符串(.LC0,.LC1)

.global:声明全局变量(main)

.type:声明一个符号是数据类型还是函数类型

3.3.2 数据和赋值

3.3.2.1 字符串

在下述函数中:

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

对应汇编代码:

leaq  .LC0(%rip), %rdi

leaq  .LC1(%rip), %rdi

3.3.2.2 变量

全局变量与静态变量:

初始化的全局变量和静态变量储存在.data节,它的初始化不需要汇编语句,而是直接完成的。未初始化的全局变量和静态变量储存在.bss节。

局部变量:

       局部变量存储在寄存器或栈中。程序中的局部变量i定义

           int i;

在汇编代码中:

此处是循环前i=0的操作,i被保存在栈当中、%rbp-4的位置上。

3.3.3 算数操作

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

在每次循环执行的内容结束后,对i进行一次自加,栈上存储变量i的值加1

3.3.4 条件操作和控制转移

程序中判断传入参数argc是否等于4,源代码为:

汇编代码为:

je用于判断cmpl产生的条件码,若两个操作数的值相等则跳转到指定地址;如果不相等则继续顺序执行。

       for循环中的循环执行条件

汇编代码为:

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

3.3.5 数组/指针/结构操作

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

int main(int argc, char *argv[]) {}

在argv数组中,argv[0]指向输入程序的路径和名称,argv[1],argv[2],argv[3]分别表示命令行参数,对应学号和姓名以及sleep的时间。

因为char* 数据类型占8个字节,根据

对比原函数可知通过%rbp-0x8,%rbp-0x16,%rbp-0x24分别保存了argv[1],argv[2]和argv[3]这三个命令行字符串参数。

3.3.6 函数操作

X86-64中,过程调用传递参数规则:第1~6个参数依次储存在%rdi、%rsi、%rdx、%rcx、%r8、%r9这六个寄存器中,剩下的参数保存在栈当中。

main函数:

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

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

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

源代码:

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

汇编代码:

可见argc存储在%edi中,argv存储在%rsi中。

printf函数

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

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

源代码1:

汇编代码1:

.LC0:

.LFB6:

    cmpl    $4, -20(%rbp)

    je  .L2

    leaq    .LC0(%rip), %rdi

    call    puts@PLT

源代码2:

汇编代码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,再执行退出命令

源代码:

    exit(1);

汇编代码:

.LFB6:

    movl    $1, %edi

    call    exit@PLT

sleep函数:

参数传递:传入参数argv[3]

函数调用:for循环下被调用

源代码:

    sleep(atoi(argv[3]));

汇编代码:

.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

源代码:

    getchar();

汇编代码:

.L3:

    call    getchar@PLT

3.4 本章小结

本章主要介绍了编译的概念以及过程。介绍了汇编代码如何实现常量、变量、传递参数以及分支和循环。编译程序所做的工作,就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其适当优化再翻译成等价的中间代码表示或汇编代码表示。通过了解的编译器的工作机制,可以更清楚地理解C语言代码是如何转化为汇编代码的。

(第32分)

第4章 汇编

4.1 汇编的概念与作用

概念

汇编代码文件(由汇编指令构成)称为汇编语言程序,汇编程序(汇编器)用来将汇编语言源程序转换为机器指令序列(机器语言程序),这一过程称为汇编。汇编指令与机器指令一一对应,前者是后者的符号表示,他们都属于机器级指令,所构成的程序称为机器级代码。

作用

汇编就是将高级语言转化为机器可直接识别执行的代码文件的过程,汇编器将.s 汇编程序翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在.o 目标文件中,.o 文件是一个二进制文件,它包含程序的指令编码。

4.2 在Ubuntu下汇编的命令

命令:as hello.s -o hello.o或gcc -c hello.s -o hello.o

图8 终端命令

4.3 可重定位目标elf格式

4.3.1命令:readelf -a hello.o > ./helloelf.txt

   

图9 终端命令

4.3.2 ELF头:

ELF头位于ELF文件开始,包含文件结构说明信息,分32位系统对应结构和64位系统对应结构,定义了ELF魔数,版本,小/大端,操作系统平台,目标文件的类习性,机器结构类型,程序执行的入口地址,程序头表(段头表)的起始位置和长度,节头表的起始位置和长度等。

图10 helloelf.txt

4.3.3 节头目表

描述了每个节的节名,在文件中的偏移,大小,访问属性,对齐方式等。

图11 节头目录

4.3.4 重定位节:

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

本程序需要重定位的信息有:.rodata中的模式串,puts,exit,printf,atoi,sleep,getchar这些符号。

图12 重定位节

4.3.5 符号表:

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

图13 符号表

4.4 Hello.o的结果解析

命令:objdump -d -r hello.o > hellodump.s

图14 终端命令

图15 hellodump.s

分析hello.o的反汇编,并与第3章的 hello.s进行对照分析:

  1. 分支转移:跳转语句之后,hello.s中是.L2和.LC1等段名称,而反汇编代码中跳转指令之后是PC相对地址。
  2. 函数调用:hello.s中,call指令使用的是函数名称,而反汇编代码中call指令后面跟了预添值0,并用一条重定位信息指明重定位方式是PC相对地址还是绝对地址。函数只有在链接之后才能确定运行执行的地址,链接后将预添值修改为PC相对值或者绝对地址。

4.5 本章小结

本章对汇编结果进行了详尽的介绍。经过汇编器的操作,汇编语言转化为机器语言,hello.o可重定位目标文件的生成为后面的链接做了准备。通过对比hello.s和hello.o反汇编代码的区别,更深刻地理解了汇编语言到机器语言的转变,以及过程中为链接做出的准备,对可重定位目标elf格式进行了详细的分析,侧重点在重定位项目上。同时对hello.o文件进行反汇编,将hellodump.s与之前生成的hello.s文件进行了对比。使得我们对该内容有了更加深入地理解。

(第41分)

第5章 链接

5.1 链接的概念与作用

链接器概念:

链接是将不同文件的代码和数据部分收集起来并组合成一个单一文件的过程。链接过程分为符号解析和重定位两步。符号解析:将每一个符号引用与一个确定和符号定义建立关联。重定位:将多个代码段和数据段分别合并为一个单独的代码段和数据段,计算每个定义的符号在虚存中的绝对地址,将可执行文件中符号引用处的地址修改为重定位后的地址信息。

链接器作用:

令源程序节省空间而未编入的常用函数文件(如printf.o)进行合并,生成可以正常工作的可执行文件。这令分离编译成为可能,节省了大量的工作空间。

5.2 在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

图16 终端命令

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

    ELF文件头:与.o文件不同的是,可执行目标文件的elf头中给出了程序入口地址,而.o文件中入口地址永远是0.

图17 终端命令

节头:

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

图18 节头(elf.txt)

符号表:

图19 符号表(elf.txt)

5.4 hello的虚拟地址空间

使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。  

图20 edb加载hello

从下图可以看出

图21 Data Dump

0x00400000开始,程序被载入,其中节的排布与Address中的声明相同。

图22 程序头表

上图为elf中的程序头表,其中记录了运行时加载的内容,同时提供动态链接的信息。每一行都提供了各个段的虚拟空间和物理内存的大小,标志位,是否对齐,读写权限的信息。

PHDR:程序头表

INTERP:需要调用的解释器(如动态链接器)

LOAD:表示需要从二进制文件映射到虚拟空间的段,其中保存了常量数据和目标代码等内容。

DYNAMIC:动态链接器使用的信息。

NOTE:辅助信息

GUN_STACK:栈是否可执行的标志。

GUN_RELRO:指定重定位之后的只读的内存区域。

5.5 链接的重定位过程分析

命令:objdump -d -r hello > dump.s

Hello的反汇编文件与hello.o的相比多了以下内容, 经查找有关资料,补充了其功能:

.interp:ld.so的路径

.note.ABI-tag:Linux特有的段

.hash:符号的哈希表

.gnu.hash:GNU中拓展符号的哈希表

.dynsym: 运行时/动态符号表

.dynstr:存放.hynsym节中的符号名称

.gnu.version:符号版本

.gun.version_r:符号引用版本。

.rela.dyn: 运行时/动态重定位表

.rela.plt:.plt节的重定位条目

.init:程序初始化需要执行的代码

.plt:动态链接-过程链接表

.fini: 当程序正常终止时需要执行的代码

.eh_frame:异常展开和源语言的信息

.dynamic:存放被ld.so使用的动态链接信息

.got:存放程序中跟变量全局偏移量

.got.plt:存放程序中函数的偏移量

.data:初始化过的全局变量或生命过的函数。

5.6 hello的执行流程

ld-2.27.so!_dl_start

ld-2.27.so!_dl_init

hello!_start

libc-2.27.so!__libc_start_main

-libc-2.27.so!__cxa_atexit

-libc-2.27.so!__libc_csu_init

hello!_init

libc-2.27.so!_setjmp

-libc-2.27.so!_sigsetjmp

–libc-2.27.so!__sigjmp_save

hello!main

hello!puts@plt

hello!exit@plt

ld-2.27.so!_dl_runtime_resolve_xsave

-ld-2.27.so!_dl_fixup

–ld-2.27.so!_dl_lookup_symbol_x

libc-2.27.so!exit

5.7 Hello的动态链接分析

在对hello的readelf分析中得知,.got表的地址为0x0000000000403ff0,通过edb中对Data Dump窗口跳转,定位到GOT表处。

 Got表的相关信息:

调用__init后的got表。

在开始edb的调试后,初始的地址0x00403ff0全为0。对于动态共享链接库中PIC函数,编译器没有办法预测函数的运行地址,所以需要添加重定位记录,等待动态链接器的处理,为避免运行时修改调用的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表PLT + 全局变量偏移表GOT实现函数的动态链接,GOT中存放目标函数的地址,PLT使用该地址跳转到目标位置,其中GOT[1]指向重定位表,GOT[2]指向动态链接器ld-linux.so运行地址。

分析hello程序的动态链接项目,通过edb调试,分析在dl_init前后,这些项目的内容变化。要截图标识说明。

5.8 本章小结

本章主要了解温习了在linux中链接的过程。通过查看hello的虚拟地址空间,并且对比hello与hello.o的反汇编代码,更好地掌握了链接与之中重定位的过程。不过,链接远不止本章所涉及的这么简单,就像是hello会在它运行时要求动态链接器加载和链接某个共享库,而无需在编译时将那些库链接到应用中。

(第51分)

第6章 hello进程管理

6.1 进程的概念与作用

进程的概念:

进程是指程序的一次运行过程,是动态的概念,且同一个程序处理不同的数据,会产生不同的进程,进程有自己的生命周期,由任务启动而创建,随任务完成而消亡,它所占用的资源也随进程终止而释放。

进程的作用:

每次运行程序时,shell创建一新进程,在这个进程的上下文切换中运行这个可执行目标文件。应用程序也能够创建新进程,并且在新进程的上下文中运行它们自己的代码或其他应用程序。进程提供给应用程序的关键抽象:一个独立的逻辑控制流,如同程序独占处理器;一个私有的地址空间,如同程序独占内存系统。进程的引入简化了编程,编译,链接,共享和加载等整个过程。

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

作用:命令行解释器,连接用户和操作系统以及内核,其基本功能是解释并运行用户的指令。

流程:

  1. Shell进程读取用户由键盘输入的命令行
  2. 分析命令行字符串,获得命令行参数,并构造argv和envp
  3. 检查第一个命令行参数是否为一个内置的shell命令
  4. 如果不是,则调用fork函数创建新进程,其进程虚拟地址空间与父进程完全一致。
  5. 在子进程中,调用execve函数在当前进程的上下文中加载指定程序,将程序的对应节通过内存映射加载到虚拟地址空间。
  6. 如果用户没要求后台执行,则shell使用waitpid等待进程终止后返回。如果要求后台执行,则shell直接返回。

6.3 Hello的fork进程创建过程

根据shell的处理流程,可以推断,输入命令执行hello后,父进程如果判断不是内部指令,即会通过fork函数创建子进程。子进程与父进程近似,并得到一份与父进程用户级虚拟空间相同且独立的副本——包括数据段、代码、共享库、堆和用户栈。父进程打开的文件,子进程也可读写。二者之间最大的不同在于PID的不同。Fork函数只会被调用一次,但会返回两次,在父进程中,fork返回子进程的PID,在子进程中,fork返回0。

6.4 Hello的execve过程

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

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

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

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

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

  1. 删除已存在的用户区域(自父进程独立)。
  2. 映射私有区:为Hello的代码、数据、.bss和栈区域创建新的区域结构,所有这些区域都是私有的、写时才复制的。
  3. 映射共享区:比如Hello程序与标准C库libc.so链接,这些对象都是动态链接到Hello的,然后再用户虚拟地址空间中的共享区域内。
  4. 设置PC:exceve做的最后一件事就是设置当前进程的上下文中的程序计数器,使之指向代码区域的入口点。

6.5 Hello的进程执行

逻辑控制流:

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

用户模式和内核模式:

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

上下文:

分为系统级上下文和用户级上下文。系统级上下文,包括内核代码和数据,与进程相关的数据结构(进程的标识信息,现场信息,控制信息等),用户级上下文包括用户栈,共享库映射区域,运行时堆,数据区,代码区等。

上下文切换:

当内核选择一个新的进程运行时,则内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程:

1) 保存以前进程的上下文

2)恢复新恢复进程被保存的上下文

3)将控制传递给这 个新恢复的进程 ,来完成上下文切换

Hello进程的执行:

Hello进程执行时,处于用户模式,此时PC根据hello的逻辑控制流一条条执行hello的.text节中的指令,直到运行至sleep函数时,此时由于sleep函数属于内核函数,因此会在此处发生进程的上下文切换,由hello进程切换到sleep进程,由用户模式切换到内核模式,sleep函数执行结束后,内核发送一个SIGCHLD信号给hello进程并将控制重新传递给hello进程,返回到hello进程的下一条指令处继续执行。当hello执行到getchar()函数时,实际落脚到执行输入流是stdin的系统调用read,该函数也属于内核级函数,因此此时又会发生进程的上下文切换,在进行read调用之后陷入内核,内核中的陷阱处理程序请求来自键盘缓冲区的DMA传输,并且安排在完成从键盘缓冲区到内存的数据传输后,中断处理器。此时进入内核模式,内核执行上下文切换,切换到其他进程。当完成键盘缓冲区到内存的数据传输时,引发一个中断信号,此时内核从其他进程进行上下文切换回hello进程。

6.6 hello的异常与信号处理

正常运行状态:

图23 正常状态

异常状态:

类别

原因

异步/同步

返回行为

中断

来自I/O设备的信号

异步

总是返回到下一条指令

陷阱

有意的异常

同步

总是返回到下一条指令

故障

潜在可恢复的错误

同步

可能返回到当前指令

终止

不可恢复的错误

同步

不会返回

处理方式:

中断处理方式:

陷阱处理方式:

故障处理方式:

终止处理方式:

按下Ctrl+Z进程收到 SIGSTP 信号,hello 进程挂起。用ps查看其进程PID,可以发现hello的PID是60143;再用jobs查看此时hello的后台 job号是1,调用 fg 1将其调回前台。

Ctrl+C进程收到 SIGINT 信号,结束 hello。在ps中查询不到其PID,在job中也没有显示,可以看出hello已经被彻底结束。

中途乱按:只是将屏幕的输入缓存到缓冲区。

Kill命令:挂起的进程被终止,在ps中无法查到到其PID。

6.7本章小结

本章了解了hello进程的执行过程。主要讲hello的创建、加载和终止,通过键盘输入。程序是指令、数据及其组织形式的描述,进程是程序的实体。可以说,进程是运行的程序。在hello运行过程中,内核有选择对其进行管理,决定何时进行上下文切换。也同样是在hello的运行过程中,当接受到不同的异常信号时,异常处理程序将对异常信号做出相应,执行相应的代码,每种信号都有不同的处理机制,对不同的异常信号,hello也有不同的处理结果。我们对hello执行过程中产生信号和信号的处理过程有了更多的认识,对使用linux调试运行程序也有了更多的新得。

(第61分)

第7章 hello的存储管理

7.1 hello的存储器地址空间

  1. 逻辑地址

逻辑地址(Logical Address)是指由程序hello产生的与段相关的偏移地址部分,是由一个段选择符加上一个指定段内相对地址的偏移量,表示为 [段选择符:段内偏移量]。

  1. 线性地址

线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。程序hello的代码会产生逻辑地址,或者说是(即hello程序)段中的偏移地址,它加上相应段的基地址就生成了一个线性地址。逻辑地址和线性地址时虚拟地址的两种不同表示形式,描述的都是虚拟地址空间中的一个存储地址。

  1. 物理地址

计算机系统的主存和磁盘被组织成一个由M个连续的字节大小的单元组成的数组,每个字节都有一个唯一的物理地址,用于内存芯片级的单元寻址,与处理器和CPU链接的地址总线相对应。

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

一个逻辑地址由两部分组成,段标识符,段内偏移量。段标识符是一个16位长的字段组成,称为段选择符,其中前13位是一个索引号。后面三位包含一些硬件细节。
    索引号,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段。
    这里面,我们只用关心Base字段,它描述了一个段的开始位置的线性地址。
    全局的段描述符,放在“全局段描述符表(GDT)”中,一些局部的段描述符,放在“局部段描述符表(GDT)”中。
    GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。
   给定一个完整的逻辑地址段选择符+段内偏移地址,
   看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。
   拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。

把Base + offset,就是要转换的线性地址了

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

线性地址到物理地址的变换是通过分页的方式实现的。

页式管理是一种内存空间存储管理的技术,页式管理分为静态页式管理和动态页式管理。将各进程的虚拟空间划分成若干个长度相等的页(page),页式管理把内存空间按页的大小划分成片或者页面(page frame),然后把页式虚拟地址与内存地址建立一一对应页表,并用相应的硬件地址变换机构,来解决离散地址变换问题。页式管理采用请求调页或预调页技术实现了内外存存储器的统一管理。

CPU发送一个虚拟地址给地址翻译器MMU,MMU将一个n位的虚拟地址划分为一个p位的虚拟页面偏移(VPO),一个n-p位的虚拟页号(VPN),MMU利用VPN选择适当的PTE,例如VPN 0选择PTE 0。根据PTE,我们知道虚拟页的信息,如果虚拟页是已缓存的,那直接将页表条目的物理页号和虚拟地址的VPO串联起来就得到一个相应的物理地址。这里的VPO和PPO是相同的。如果虚拟页是未缓存的,会触发一个缺页故障。调用一个缺页处理子程序将磁盘的虚拟页重新加载到内存中,然后再执行这个导致缺页的指令。

图24 页式管理流程图

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

每次CPU产生一个虚拟地址,MMU(内存管理单元)就必须查阅一个PTE(页表条目),以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这会从内存多取一次数据,代价是几十到几百个周期。如果PTE碰巧缓存在L1中,那么开销就会下降1或2个周期。然而,许多系统都试图消除即使是这样的开销,它们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓存器(TLB)。

多级页表:

将虚拟地址的VPN划分为相等大小的不同的部分,每个部分用于寻找由上一级确定的页表基址对应的页表条目。

图25 使用k级页表进行翻译

解析VA,利用前m位vpn1寻找一级页表位置,接着一次重复k次,在第k级页表获得了页表条目,将PPN与VPO组合获得PA

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

CPU发送一条虚拟地址,随后MMU按照上述操作获得了物理地址PA。根据cache大小组数的要求,将PA分为CT(标记位)CI(组号),CO(偏移量)。根据CI寻找到正确的组,比较每一个cacheline是否标记位有效以及CT是否相等。如果命中就直接返回想要的数据,如果不命中,就依次去L2,L3,主存判断是否命中,当命中时,将数据传给CPU同时更新各级cache的cacheline(如果cache已满则要采用换入换出策略)。

图26 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)映射私有区域:

代码和初始化数据映射到.text和.data区.bss和栈映射到匿名文件

5)映射共享区域:

将共享库文件的代码和数据映射到虚拟内存空间的共享库映射区域。

6)设置程序计数器(PC)

exceve做的最后一件事是设置当前进程的上下文中的程序计数器,是指指向代码区域的入口点。而下一次调度这个进程时,他将从这个入口点开始执行。Linux将根据需要换入代码和数据页面。

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

页面命中完全是由硬件完成的,而处理缺页是由硬件和操作系统内核协作完成的:

图27 缺页中断处理

整体的处理流程:

  1. 处理器生成一个虚拟地址,并将它传送给MMU
  2. MMU生成PTE地址,访问主存或TLB
  3. 高速缓存/主存向MMU返回PTE
  4. PTE中的有效位是0,所以MMU出发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序。
  5. 缺页处理程序确认出物理内存中的牺牲页,如果这个页已经被修改了,则把它换到磁盘。
  6. 缺页处理程序页面调入新的页面,并更新内存中的PTE
  7. 缺页处理程序返回到原来的进程,再次执行导致缺页的命令。CPU将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面已经换存在物理内存中,所以就会命中。

7.9动态存储分配管理

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

分配器有两种基本风格:显式分配器和隐式分配器。显式分配器:要求应用显式地释放任何已分配的块。例如C语言的malloc程序包,包括malloc函数free函数等等。隐式分配器:应用检测到已分配块不再被程序所使用,就释放这个块。又称为垃圾收集器。

堆中空闲块的组织形式有多种,包括隐式空闲链表,显式空闲链表以及分离的空闲链表的形式。

带边界标签的隐式空闲链表:

隐式空闲链表通过每个块的头部信息来组织堆。找到一个空闲块的方法可以是首次适配,下一次适配也可以是最佳适配。首次适配,指从头开始搜索空闲链表,选择第一个合适的空闲块,搜索时间与总块数成线性关系,在靠近链表起始处会留下小空闲块的碎片。下一次适配和首次适配相似,只是每次从链表中上一次查询结束的地方开始。最佳适配会查询链表,选择一个最好的空闲块。

分配空闲块时,采用分隔的方法,把空闲块分未两个部分,一部分用来记录想要分配的信息,另一部分仍设置为空闲块,放入隐式空闲链表中。

释放空闲块时,采用清除分配块的头部和脚部中的分配位信息(将分配位置为0),再进行空闲块合并的方法,实现空间的回收再利用。

显式空闲链表:

显式空闲链表结构将堆组织成一个双向空闲链表,在每个空闲块的主体中,都包含一个pred(前驱)和succ(后继)指针。

使用双向链表而不是隐式空闲链表,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。不过,释放一个块的时间可以是线性的,也可能是个常数,这取决于空闲链表中块的排序策略。

一种方法是用后进先出(LIFO)的顺序维护链表,将新释放的块放置在链表的开始处。另一种方法是按照地址顺序来维护链表,其中链表中每个块的地址都小于它后继的地址。

分离的空闲链表:

分配器维护空闲链表数组,每个大小类一个空闲链表。当分配器需要一个大小为n的块时:搜索相应的空闲链表,使其满足m > n,如果找到了合适的块:拆分块,并将剩余部分插入到适当的可选列表中。如果找不到合适的块, 就搜索下一个更大的大小类的空闲链表,直到找到为止。如果空闲链表中没有合适的块:向操作系统请求额外的堆内存 (使用sbrk()),从这个新的堆内存中分配出 n 字节,将剩余部分放置在适当的大小类中。

7.10本章小结

本章主要介绍了 hello 的存储器地址空间、 intel 的段式管理、 hello 的页式管理,在指定环境下介绍了 VA PA 的变换、物理内存访问,还介绍 hello 进程 fork 时的内存映射、 execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。

(第7 2分)

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

一个 Linux文件就是一个m字节的序列: B0 , B1 , .... , Bk, .... , Bm-1。所有的I/O设备都被模型化为文件:/dev/sda2(用户磁盘分区)/dev/tty2(终端)。I/O操作可看作对相应文件的读或写。

Linux内核给出的一个简单、低级的应用接口,能够以统一且一致的方式执行 I/O操作,包括打开和关闭文件open()和close();读写文件read()和write();改变当前的文件位置,指示文件要读写位置的偏移量lseek()。

8.2 简述Unix IO接口及其函数

1. 打开文件:进程向系统要求打开一个文件,系统会返回一个非负整数,此数为描述符,他在后续的鱼这个文件有关的任何操作中代表这个文件,内核记录有关于这个文件的所有信息。

Open函数将文件名转换为文件描述符,并且返回描述符数字。Flags表示进程访问文件方式。
2. 改变位置:对于每个打开的文件,内核都会记录一个位置,此位置为从所有文件的开头开始计算的情况下,此文件的偏移量。
3. 读写文件:
1)读操作:从文件中复制需要读取的字节到内存中。

2)写操作:从内存总复制文件大小个字节到对应文件。读写操作都用到之前记录的文件位置。

4. int close(fd),fd是需要关闭的文件的描述符,close返回操作结果。

8.3 printf的实现分析

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

图28 printf函数

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

接下来是write函数:

图29 write函数

在printf中调用系统函数write(buf,i)将长度为i的buf输出,在write函数中,将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址,

int INT_VECTOR_SYS_CALLA代表通过系统调用syscall。

查看syscall函数体:

图30 syscall函数体

syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。

字符显示驱动子程序将通过ASCII码在字模库中找到点阵信息将点阵信息存储到vram中。

显示芯片会按照一定的刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

于是我们的打印字符串就显示在了屏幕上。

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

从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.

字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。

显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

getchar 的源代码为:

图31 getchar函数

异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码转换成 ASCII 码,保存到系统的键盘缓冲区之中。

getchar 函数落实到底层调用了系统函数 read,通过系统调用read读取存储在 键盘缓冲区中的ASCII码直到读到回车符然后返回整个字串,getchar进行封装, 大体逻辑是读取字符串的第一个字符然后返回。

8.5本章小结

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

(第81分)

结论

Hello的一生可谓变化多端:

    1. 程序员编写代码,得到hello.c文件
    2. hello.c经过预处理,拓展得到hello.i文本文件
    3. hello.i经过编译,得到汇编代码hello.s汇编文件
    4. hello.s经过汇编,得到二进制可重定位目标文件hello.o
    5. hello.o经过链接,生成了可执行文件hello
    6. 在shell命令行中输入120L020820 庞博 1[enter]
    7. shell进程调用fork函数,创建子进程;并由execve函数加载hello程序到当前进程的上下文中。
    8. hello再运行时会调用一些函数以及进程的上下文切换。比如printf和getchar函数调用时与linux I/O的设备密切相关,sleep和exit函数调用时会发生用户模式到内核模式的切换等。
    9. hello进程执行完毕,最终被shell父进程回收,内核会收回为其创建的所有信息。
    10. 当hello程序执行printf函数时,会调用 malloc 向动态内存分配器申请堆中的内存。
    11. 当程序在运行的时候我们输入Ctrl+c,内核会发送SIGINT信号给进程并终止前台作业。当输入Ctrl+z时,内核会发送SIGTSTP信号给进程,并将前台作业停止挂起。
    12. 当子进程执行完成时,内核安排父进程回收子进程,将子进程的退出状态传递给父进程。内核删除为这个进程创建的所有数据结构。

对计算机系统的设计与实现的深切感悟:计算机系统的设计思想和实现都是基于抽象实现的。从最底层的信息的表示用二进制表示抽象开始,到实现操作系统管理硬件的抽象:进程是对处理器、主存和I/O设备的抽象。虚拟内存是对主存和磁盘设备的抽象。文件是对I/O设备的抽象。

计算机系统的设计精巧:为了解决快的设备存储小、存储大的设备慢的不平衡,设计了高速缓存来作为更底层的存储设备的缓存,大大提高了CPU访问主存的速度。

计算机系统的设计考虑全面:计算机系统设计考虑一切可能的实际情况,设计出一系列的满足不同情况的策略。比如写回和直写,写分配和非写分配,直接映射高速缓存和组相连高速缓存等等。

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

(结论0分,缺失 -1分,根据内容酌情加分)

附件

文件的作用

文件名

预处理后的文件

hello.i

编译之后的汇编文件

hello.s

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

hello.o

链接之后的可执行目标文件

hello

hello.o 的 ELF 格式

helloelf.txt

hello.o 的反汇编代码

hellodump.s

hello 的反汇编代码

dump.s

Hello的ELF格式

elf.txt

列出所有的中间产物的文件名,并予以说明起作用。

(附件0分,缺失 -1分)

参考文献

为完成本次大作业你翻阅的书籍与网站等

[1] https://www.cnblogs.com/diaohaiwei/p/5094959.html

[2] 深入理解计算机系统原书第3版-文字版.pdf

(参考文献0分,缺失 -1分)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值