计算机系统大作业

** 由于采用静态部署,需要看图片详细分析的小伙伴请移步个人博客网站:**
个人博客
题目:程序人生-Hello’s P2P

学号:

姓名:熊峰

摘要:

hello程序作为最简单的、最经典的程序,在实现上非常简单,但即使是最简单的hello.c程序,也经历了复杂的一生,它需要计算机上的几乎所有的硬件设备与操作系统等协同工作,才能运行起来。
本文将结合《深入理解计算机系统》,深入分析Linux环境下,hello的P2P,020的整个过程,进而将理论知识与实践紧密结合。

关键词:预处理、编译、汇编、链接、进程管理、存储管理、IO管理

第一章 概述

1.1 Hello简介

hello程序的生命周期是从一个高级C语言程序开始的,然而为了在系统上运行hello.c程序,每条C语句都必须被其他程序转化为一系列的低级机器语言指令。然后这些指令按照一种成为可执行目标程序的格式打好包,并以二进制磁盘文件的形式存放起来。

hello程序的P2P过程:
hello程序经过预处理、编译、汇编、链接,生成可执行文件,在shell中运行hello程序时,shell通过调用系统调用来执行我们的请求,系统调用会将控制权传递给操作系统,操作系统保存shell进程的上下文,使用fork函数,创建一个新的hello进程及其上下文,完成程序到进程的转变P2P(From Program to Process)。

hello程序的020过程:
shell执行可执行目标文件,使用execve函数加载进程,映射虚拟内存,进入程序入口后程序载入物理内存。进入main函数执行目标代码,CPU为运行的hello分配时间片执行逻辑控制流。当程序运行结束后,shell父进程负责回收hello进程,内核删除相关数据结构。

1.2 环境与工具

硬件环境:Intel® Core™ i7-9700K CPU @ 3.60GHz;RAM 16.0 GB (15.8 GB 可用)

软件环境:Win 10 20H2;Ubuntu 20.04.2 LTS;WSL2;VMware workstation

开发与调试工具:vim; gcc; VSCode; readelf; ld; as; edb

1.3 中间结果

列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-reLbOyyb-1624470134853)(https://i.loli.net/2021/06/23/oFjqbZx92i6zUKy.png)]

1.4 本章小结

本章简要介绍了hello程序的P2P,020的过程、本次实验的环境工具,以及实验过程中,生成的中间结果文件。

第二章 预处理

2.1 预处理的概念与作用

预处理器(cpp)根据以#开头的命令,修改原始的C程序。比如hello.c中第一行的#include <stdio.h>命令告诉预处理器读取系统头文件stdio.h中的内容,并把它直接插入到程序文本中。结果就得到了另一个C程序,通常是以.i作为文件的扩展名。

预处理过程是整个编译过程的第一步。预处理是指在进行编译的第一遍扫描之前所做的工作。预处理指令指示在程序正式编译前就由编译器进行的操作,可放在程序中任何位置。C语言提供多种预处理功能,预处理器会分析预处理指令:包含文件、条件编译、宏定义与扩展、特殊宏与指令、Token字符串化、Token连接、用户定义的编译错误与警告以及编译器相关的预处理特性等,并去除源代码中的注释。

2.2 在Ubuntu下预处理的命令

首先启用WSL,启动Ubuntu 20.04.2 LTS,如图2-1所示.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yU8xLC5I-1624470134856)(https://i.loli.net/2021/06/23/O6C1jmig4r8aMSL.png)]

在Ubuntu下对hello.c进行预处理,应使用如下命令:

gcc -E hello.c -o hello.i 或 cpp hello.c > hello.i

在wsl中键入命令,得到经过预处理后的文件,如图2-2所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MKq4zKDp-1624470134857)(https://i.loli.net/2021/06/23/TD1rmQFWUbhv23c.png)]

2.3 Hello的预处理结果解析

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d9QU26wV-1624470134858)(https://i.loli.net/2021/06/24/1ewApST27mCdzMP.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B7inAa3W-1624470134859)(https://i.loli.net/2021/06/24/3zUM7J1dsxrpNOZ.png)]

如图2-4所示,使用Vim查看得到的与处理文件,可以发现hello.i已经被扩展到了3060行。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yUNsem0C-1624470134860)(https://i.loli.net/2021/06/24/vJZ7s8a9zmSApxy.png)]

如图2-5所示,通过Vim中的搜索功能,可以看到此时原来的main函数位于3047行。此时,hello.c程序中的头文件 stdio.h、unistd.h、stdlib.h中的内容插入到了hello.i文件中,内容包括函数声明、结构体定义、变量定义等,头文件的内容被递归展开进hello.i文件中。

2.4 本章小结

本章主要介绍了预处理的概念与作用、在Ubuntu下预处理的命令以及Hello程序经过预处理后的简要分析。

第三章 编译

3.1 编译的概念与作用

编译的概念:
编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析以及优化后生成相应的汇编代码文件。经历如下过程:

  • 词法分析:词法分析使用程序实现词法扫描,它会按照用户之前描述好的词法规则将输入的字符串分割成一个个记号。产生的记号一般分为:关键字、标识符、字面量(包含数字、字符串等)和特殊符号(运算符、等号等),然后他们放到对应的表中。
  • 语法分析:语法分析器根据用户给定的语法规则,将词法分析产生的记号序列进行解析,然后将它们构成一棵语法树。对于不同的语言,只是其语法规则不一样。
  • 语义分析:语法完成了对表达式语法层面的分析,但是它不了解这个语句是否真正有意义。有的语句在语法上是合法的,但是却是没有实际的意义,比如说两个指针的做乘法运算,这个时候就需要进行语义分析,但是编译器能分析的语义也只有静态语义。
  • 中间代码生成:对于一些在编译期间就能确定的值,编译器可以将他优化。优化器会先将语法树转成中间代码。中间代码一般与目标机器和运行环境无关。
  • 目标代码生成与优化:代码生成器将中间代码转成机器代码,这个过程是依赖于目标机器的,因为不同的机器有着不同的字长、寄存器、数据类型等。最后目标代码优化器对目标代码进行优化,比如选择合适的寻址方式、使用唯一来代替乘除法、删除出多余的指令等。

编译作用:编译过程是整个程序构建的核心部分,编译成功,编译器会将源代码由文本形式转换成机器语言。

在本例中,编译器将hello.i编译成hello.s。

3.2 在Ubuntu下编译的命令

在Ubuntu下对hello.i进行编译,应使用如下命令,如图3-1所示:

gcc -S hello.i -o hello.s

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gc7FzZ7u-1624470134860)(https://i.loli.net/2021/06/24/6rf5Fszu2Ua9CIx.png)]

3.3 Hello的编译结果解析

3.3.1 汇编文件声明解析

声明 含义
.file 声明源文件
.text 代码段
.section .rodata 只读代码段
.align 对齐的格式
.global 全局变量
.string 字符串类型数据
.long long类型数据
.type 符号类型
.size 声明大小

3.3.2 整型数据类型

源码中出现多种数据类型,首先分析整型,在源代码中出现的整型有argc参数、i计数参数。

首先分析argc,根据x86-64的寄存器使用,argc应该被保存在%edi中,通过查看hello.s,如图3-2,我们可以看到此时%edi的值被保存在-20(%rbp)中,并与4做比较,用于条件跳转,若相等则跳转到.L2处,否则执行leaq及接下来的指令。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GQVAyMsq-1624470134861)(https://i.loli.net/2021/06/24/vztbQHYS5aACglk.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Id22bTEs-1624470134861)(https://i.loli.net/2021/06/24/dxalvjNCJbMiSTE.png)]

接下来分析计数参数i,如图3-3所示,i首先出现在.L2处,将0保存在-4(%rbp)中,在跳转到.L3处,如图3-4所示,用cmpl语句,使-4(%rbp)处的值与7相比,若大于7则跳出循环,作为循环的出口。并在.L4的结尾处,将-4(%rbp)进行加一的操作,使其能够正常进行循环。

实际代码中,还存在着一些立即数,如常数1用于控制循环加减等操作。

3.3.3 字符串数据类型

查看hello.s,如图3-5,可以发现在hello.s中,字符串类型数据共出现两次,保存在程序的.rodata段中.

如图3-6所示,在hello.s中,两个字符串被调用的地方。

3.3.4 数组数据类型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7u9vUbd7-1624470134863)(https://i.loli.net/2021/06/24/8S9TatGnMjNlqYk.png)]

如图3-7,查看hello.s源代码,根据x86-64使用寄存器保存参数的规则,此时argv应该保存在%rsi中,如图,发现%rsi被调用的地方。根据.L4中对addq $8, %rax的操作,可得argv中元素为八个字节(详细分析具体在3.3.8中)。此处分别调用了argv[1]和argv[2],可以看到%rax又执行了加24的操作,可以看出此时取argv[3]中的元素,并将其取到%rax中,并放入%rdi中作为参数,调用atoi函数。并将返回结果%eax放入%edi中,调用sleep函数。

3.3.5 赋值操作

如图3-8,首先查看源代码,发现存在对i赋值为0和i++的操作(具体操作在3.3.6处)。因此查看hello.s中,循环部分的操作。

通过对hello.s分析,我们可知,当argv为4时,程序会跳转到.L2处,如图所示,我们可以发现将0赋值给-4(%rbp)。

3.3.6 算数操作

如图3-10,此时用-4(%rbp)的值与7作比较,控制循环。并在小于等于的时候,跳转到.L4处。

如图3-11,我们可以发现,此时-4(%rbp)执行加一的操作,即对i执行i++的操作。

3.3.7 关系操作

如图3-12,通过对hello.c分析,一共出现两处关系操作。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X55oifAI-1624470134865)(https://i.loli.net/2021/06/24/k6tVx4zjBobi2cs.png)]

如图3-13,此处为第一处关系操作,将argc的值与4相比。若相等则跳转到.L2处,否则继续执行。

如图3-14,此处为第二处关系操作,利用-4(%rbp)处的值与7相比,控制循环的出口。

3.3.8 数组/指针/结构操作

分析源码,此处argv[1]、argv[2]和argv[3]存在数组的操作,分别用作printf和sleep函数的参数。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z7RCQreH-1624470134867)(https://i.loli.net/2021/06/24/XBnhiAe4tHLKY81.png)]

hello.s源代码如图3-17所示.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oeR0n5Fq-1624470134867)(/CSAPP/image013.png)]

如图3-17,查看hello.s源代码,根据x86-64使用寄存器保存参数的规则,此时argv应该保存在%rsi中,如图,发现%rsi被调用的地方。根据.L4中对addq $8, %rax的操作,可得argv中元素为八个字节。分析:发现,首先将%rsi的值赋给-32(%rbp),再将-32(%rbp)的值赋给%rax,再使用addq $16, %rax的指令,将%rax的指针移到argv偏移16字节的位置,再将它的值保存到%rdx,再将-32(%rbp)中的值加8,并将对应的值保存到%rsi。此处分别对应argv[1]、argv[2]。

接下来将%rax执行加24的操作,将此时的偏移移动到argv[3]处,并将取出的结果保存到%rax处,再将%rax的值赋给%rdi,此时%rdi作为atoi函数的参数,调用atoi函数,再将返回结果%rax赋给%rdi作为sleep函数的参数,再调用sleep函数。

3.3.9 控制转移

源代码中存在两次控制转移:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bQe22rEZ-1624470134867)(/CSAPP/image023.png)]

如图3-18,首先if及for实现控制转移。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YjsvZtEn-1624470134868)(/CSAPP/image024.png)]

如图3-19,图中显示的是if控制转移。若-20(%rbp)处的值等于4,则跳转到.L2,否则继续执行leaq等操作。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y0X2JjiT-1624470134868)(/CSAPP/image025.png)]

如图3-20,图中显示的是for控制转移。若-4(%rbp)处的值大于4,则调用getchar函数,否则跳转到.L4处。

3.3.10 函数调用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-set7MLGT-1624470134869)(/CSAPP/image026.png)]
如图3-21,hello.c文件中,共存在七处函数调用。

printf、exit:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jDTECDwA-1624470134869)(/CSAPP/image027.png)]

如图3-22,若argv!=4,则此时会调用puts函数,输出“用法: Hello 学号 姓名 秒数!\n”,再将1赋给%edi中,此时%edi作为参数调用exit函数,完成两次函数调用。

Printf、atoi、sleep:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8BNPkstR-1624470134869)(/CSAPP/image028.png)]

如图3-23,若此时argc为4,则在经过赋值等操作后,将跳转到.L4中,经过前面的分析,我们有此时将会调用printf函数,输出argv[1]和argv[2],并根据将argv[3]的值到存在%rdi中,调用atoi函数,在将结果保存到%edi中,%edi作为参数,调用sleep函数,此时完成了三次函数调用。

getchar:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GwmI1Lu6-1624470134870)(/CSAPP/image029.png)]

图 3 - 24
如图3-24,若此时跳出循环,则会调用getchar函数。
main:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G7dkPHQ8-1624470134870)(/CSAPP/image030.png)]

如图3-25,存在对main函数的调用。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w3ix4E2K-1624470134870)(/CSAPP/image031.png)]

如图3-26,将%rax赋值为0,作为main函数的返回值,并通过调用leave、ret指令结束main函数。故共计存在七次函数调用。

3.4 本章小结

本章详细介绍了编译的概念与作用、在Ubuntu下编译的指令,以及对hello.c的详细分析,及其所对应的汇编代码hello.s的详细分析,对其各项数据类型、赋值操作、算术操作、关系操作、数组/指针/结构操作、控制转移操作、函数调用等结合汇编代码深入分析和细致探讨。

第四章 汇编

4.1 汇编的概念与作用

汇编的概念:

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

汇编的作用:

将汇编文本程序翻译成机器语言指令并打包成可重定位目标程序。可重定位目标程序文件是一个二进制文件,他包含的字节是函数main的指令编码。

4.2 在Ubuntu下汇编的命令

在Ubuntu下对hello.s进行汇编,应使用如下命令中的一个:

 as hello.s -o hello.o gcc -c hello.s -o hello.o

在WSL中,执行该命令,如图4-1所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-scqAmg1p-1624470134871)(/CSAPP/image032.png)]

4.3 可重定位目标elf格式

分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。

首先回忆一下elf文件的结构,如图4-2所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-17IcfyUh-1624470134871)(/CSAPP/image033.png)]

在Ubuntu下读取分析hello.o文件使用如下命令,将hello.o文件的基本信息读入到hello_o.elf文件中:

 readelf -a hello.o > hello_o.elf

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V15EXYn6-1624470134872)(/CSAPP/image034.png)]

通过查看hello_o.elf文件,我们可以获得如下信息:

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AmLbBQKR-1624470134872)(/CSAPP/image035.png)]

  • Section Headers:如图4-5,节头表包括节名称,节的类型,节的属性(读写权限),节在ELF文件中所占的长度以及节的对齐方式和偏移量。我们可以使用终端指令readelf -S hello.o来查看节头表。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LLyePu9B-1624470134872)(/CSAPP/image036.png)]

  • 符号表:如图4-6所示,符号表存放在程序中定义和引用的函数和全局变量的信息。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XevmAPOG-1624470134873)(/CSAPP/image037.png)]

  • 重定位表:重定位条目包含了链接时重定位所需的全部信息:需要被重定位的代码在其段中的偏移、该段代码所对应的符号在符号表中的索引以及重定位类型、重定位时被使用到的加数。如图4-7所示,为hello.o对应的重定位表。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c4AcoD5A-1624470134873)(/CSAPP/image038.png)]

4.4 hello.o的结果解析

在Ubuntu下对hello.o进行反汇编,应使用如下命令:

objdump -d -r hello.o

并通过

objdump -d -r hello.o > hello_o.s

指令,将反汇编的代码写入hello_o.s中,如图4-8所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YM7drhwq-1624470134874)(/CSAPP/image039.png)]

通过VSCode打开hello.s与hello_o.s,并仔细对比,如图4-9所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TR65lYQW-1624470134874)(/CSAPP/image040.png)]

从图中可以看出,hello.o的反汇编代码和hello.s大致相同,在某些地方存在小部分区别。

区别如下:

  • 分支转移:hello.s文件中分支通过使用段名称跳转,而在hello.o的反汇编代码中,通过地址跳转。
    如图4-10,我们可以发现在hello.s中,分支转移通过.L2、.L3进行跳转。如图4-11,我们可以发现,在hello.o的反汇编代码中,分支转移通过计算地址,进行跳转。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tevQIzMm-1624470134874)(/CSAPP/image041.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XvLnqU5N-1624470134875)(/CSAPP/image042.png)]

  • 函数调用:如图4-12,在hello.s文件中,函数调用call只需要加函数名称,在hello.o反汇编代码中,如图4-13,call则是使用了当前指令的下一个字节。原因是因为该函数迟绑定,该函数为共享库中函数,只有运行时,动态链接器作用后才能确定相应的PLT条目地址。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nFFEaiw1-1624470134875)(/CSAPP/image043.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PifpUA6V-1624470134875)(/CSAPP/image044.png)]

  • 操作数:如图4-14,在hello.s中,操作数为十进制数,而在hello.o的反汇编代码中,如图4-15,操作数为十六进制数。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T3lmyRLk-1624470134876)(/CSAPP/image045.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cEUUCblb-1624470134876)(/CSAPP/image046.png)]

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

说明:
机器语言:二进制的机器指令的集合;

机器指令:由操作码和操作数构成的;

机器语言:灵活、直接执行和速度快。

汇编语言:主体是汇编指令,是机器指令便于记忆的表示形式,为了方便程序员读懂和记忆的语言指令。

汇编指令和机器指令在指令的表示方法上有所不同。

4.5 本章小结

本章对汇编做了详尽的介绍,详细介绍了汇编的概念、作用,实践了在Ubuntu下汇编的命令,以及详细解读了可重定位目录elf目标格式,并对hello.o的结果作出了详细的分析,将hello.s和hello.o的反汇编代码对比,并讨论不同之处,有利于我更深入的理解汇编。

第五章 链接

5.1 链接的概念与作用

链接的概念:链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时,也就是在源代码被翻译成机器代码时,也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行。在现代系统中,链接是由叫做连接器的程序自动执行的。

链接的作用:链接使分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们该边这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。

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

gcc hello.o -o hello

如图5-1使用方法一链接,5-2使用方法二链接。(最终保留通过ld链接生成的可执行文件)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9YDsFLR2-1624470134877)(/CSAPP/image047.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jTczs1D4-1624470134877)(/CSAPP/image048.png)]

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

首先获取hello文件的elf格式文件,通过如下命令:

 Readelf -a hello > hello_elf

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YUI70Qtj-1624470134878)(/CSAPP/image049.png)]

执行上述命令,如图5-3所示。

  • 读取hello的ELF头,如图5-4所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-STqywLi3-1624470134878)(/CSAPP/image050.png)]

以16字节的序列Magic开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下的部分包含帮助连接器语法分析和解释目标文件的信息。其中包含ELF头的大小、目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如x86-64)、节头部表的文件偏移,以及节头部表中条目的大小和数量。不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的条目,与链接前的ELF header相比,除了系统决定的基本信息不变,section header和paogram header都增加,并且增加了入口处的地址。

  • 读取hello的节头表,如图5-5所示。

Section Headers对hello中所有节的信息进行了声明,其中包括大小Size以及在程序中的偏移量Offset,因此根据Section Headers中的信息,我们可以用Hexedit定位到各个节所在的空间。Address为程序被载入到虚拟地址的起始空间。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GoQMPS1c-1624470134878)(/CSAPP/image051.png)]

  • 程序头部表,ELF文件头结构就像是一个总览图,描述了整个文件的布局情况,因此在ELF文件头结构允许的数值范围,整个文件的大小是可以动态增减的,告诉系统如何创建进程映像。如图5-6所示,即为hello可执行文件的程序头部表。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r0g9Psd1-1624470134879)(/CSAPP/image052.png)]

  • 动态偏移表:若目标文件参与动态链接,则其程序头表将包含一个类型为PT_DYNAMIC的元素,此段包含.dynamic节。特殊符号_DYNAMIC用于标记包含以下结构的数组的节,如图5-7所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rQo34wZy-1624470134879)(/CSAPP/image053.png)]

  • 重定位节.rela.text,一个.text节中位置的列表,包含.text节中需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。如图5-8所示,分别描述了main、puts、printf、getchar、atoi、exit、sleep函数的重定位声明。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BH1aSwNI-1624470134880)(/CSAPP/image054.png)]

  • 符号表节:目标文件的符号表包含定位和重定位程序的符号定义和符号引用所需的信息。符号表索引是此数组的下标。索引0指定表中第一项用作未定义的符号索引。如图5-9所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2qrtQEYK-1624470134880)(/CSAPP/image055.png)]

  • 动态符号表用来保存于动态链接相关的导入导出符号,不包括模块内部的符号。如图5-10所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9oPe8PFW-1624470134880)(/CSAPP/image056.png)]

5.4 hello的虚拟地址空间

  • 打开edb

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kMmddaSf-1624470134881)(/CSAPP/image057.png)]

  • 加载hello可执行文件

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wA1zQojn-1624470134881)(/CSAPP/image058.png)]

  • 观察edb的Data Dump窗口。窗口显示虚拟地址由0x401000开始,到0x401ff0结束。之间对应5.3中的节头表的声明。根据截图5-5,可得起始位置为0x401000。Data Dump窗口如图5-13所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-viKuHd8x-1624470134881)(/CSAPP/image059.png)]

  • 观察edb的Symbols的窗口,如图5-14所示,与5-5中各节所示一致。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3FPA1Ik7-1624470134882)(/CSAPP/image060.png)]

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

5.5 链接的重定位过程分析

使用objdump -d -r hello指令对hello反汇编,如图5-15所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-amKPpU38-1624470134882)(/CSAPP/image061.png)]

打开VSCode检查hello与hello.o的反汇编文件的不同。如图5-16所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eGvZgPB9-1624470134883)(/CSAPP/image062.png)]

分析hello反汇编文件与hello.o反汇编文件的区别:

  • hello的反汇编代码中,比hello.o的反汇编代码中多了.init节,.plt节,.fini节,.plt.sec等。如图5-17所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ixig4aGH-1624470134883)(/CSAPP/image063.png)]

  • hello的反汇编代码中增加了外部链接的共享库函数。例如在hello的反汇编代码中可以看到puts@plt等,如图5-18、图5-19、图5-20所示atoi函数所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Hz5TsHUB-1624470134883)(/CSAPP/image064.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7HhYgI5C-1624470134884)(/CSAPP/image065.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pBgXYgOX-1624470134884)(/CSAPP/image066.png)]

  • hello的反汇编代码相比hello.o的反汇编代码,跳转地址修改为了虚拟内存地址。如图5-18、图5-19所示。此时callq后跟的指令为0xff ff ff 26 = -0xda,且此时执行到的地址为0x401195,0x401195+0x5-0xda=0x4010c0。此时0x4010c0为atoi@plt函数的地址。

  • hello中节的起始位置修改为了虚拟地址,hello.o反汇编代码中为相对偏移地址。如图5-21所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mYeXAiSL-1624470134884)(/CSAPP/image067.png)]

hello如何重定位的:

重定位节和符号定义:在这一步,链接器将所有相同类型的节合并为同一类型的新的聚合节。

重定位节中的符号引用:在这一步中,连接器修改代码节和数据节中对每个符号的引用,使得它们指向正确运行时的地址,要执行这一步,链接器依赖于可重定位目标模块中成为重定位条目的数据结构。

5.6 hello的执行流程

函数 地址
<_init> 0x0000000000401000
<puts@plt> 0x0000000000401090
<printf@plt> 0x00000000004010a0
<getchar@plt> 0x00000000004010b0
<atoi@plt> 0x00000000004010c0
<exit@plt> 0x00000000004010d0
<sleep@plt> 0x00000000004010e0
<_start> 0x00000000004010f0
<_dl_relocate_static_pie> 0x0000000000401120
<main> 0x0000000000401125
<__libc_csu_init> 0x00000000004011c0
<__libc_csu_fini> 0x0000000000401230
<_fini> 0x0000000000401238

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s9pdykd6-1624470134885)(/CSAPP/image068.png)]

并在使用gdb调试hello的时候,对每个函数加上断点,如图5-23所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0O6mFw1m-1624470134885)(/CSAPP/image069.png)]

使用r 1190200708 熊峰 2开始调试,如图5-24所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6pJMMtot-1624470134885)(/CSAPP/image070.png)]

运行流程如下,如图5-25所示:首先调用0x401000处的_init函数,然后调用0x4010f0处的_start函数,然后调用0x4011c8处的__libc_csu_init函数,然后再调用_init函数,之后就进入了0x401125处的main函数。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3O8BOd5r-1624470134886)(/CSAPP/image071.png)]

如图5-26,在进入main函数后,首先调用位于0x4010a0处的printf@plt函数。然后调用0x4010c0处的atoi@plt函数,然后调用位于0x4010e0处的sleep@plt函数,循环八次。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3Y9Inwhe-1624470134886)(/CSAPP/image072.png)]

如图5-27,在最后一次调用sleep@plt函数后,调用位于0x4010b0处的getchar函数@plt函数。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gRRFBAWU-1624470134886)(/CSAPP/image073.png)]

如图5-28,最后调用_fini函数,程序调试结束。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xJJ8ZbGa-1624470134887)(/CSAPP/image074.png)]

5.7 hello的动态链接分析

动态链接说明:共享库是一个目标模块,在运行或加载时,可以加载到任意的内存地址,并和一个在内存中的程序链接起来,这个过程称为动态链接,由动态连接器执行。共享库是以两种不同的方式来“共享”的。首先,在任何给定的文件系统中,对于一个库只有一个.so文件。所有引用该库的可执行目标文件共享这个.so文件中的代码和数据,而不是像静态库的内容那样被复制和嵌入到引用它们的可执行的文件中。其次,在内存中,一个共享库的.text节的一个副本可以被不同的正在运行的进程共享。

对于动态链接库中的函数,编译器无法预测函数运行时的地址,因此动态链接库的函数在程序执行的时候才会确定地址,所以需要为其添加重定位记录,并等待动态连接器处理。为避免运行时候修改调用模块的代码段,链接器采用延迟绑定的策略。延迟绑定通过全局偏移量表(GOT)和过程链接表(PLT)实现。如果一个目标模块调用定义在共享库中的任何函数,那么就有自己的GOT和PLT。前者是数据段的一部分,后者是代码段的一部分。GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。PLT是一个数组,其中每个条目是16字节代码。每个库函数都有自己的PLT条目,PLT[0]是一个特殊的条目,跳转到动态链接器中。从PLT[2]开始的条目调用用户代码调用的函数。
GOT同样是一个数组,每个条目是8字节的地址,和PLT联合使用时,GOT[2]是动态链接在ld-linux.so模块的入口点,其余条目对应于被调用的函数,在运行时被解析。每个条目都有匹配的PLT条目。

当某个动态链接函数第一次被调用时先进入对应的PLT条目例如PLT[2],然后PLT指令跳转到对应的GOT条目中例如GOT[4],其内容是PLT[2]的下一条指令。然后将函数的ID压入栈中后跳转到PLT[0]。PLT[0]通过GOT[1]将动态链接库的一个参数压入栈中,再通过GOT[2]间接跳转进动态链接器中。动态链接器使用两个栈条目来确定函数的运行时位置,用这个地址重写GOT[4],然后再次调用函数。经过上述操作,再次调用时PLT[2]会直接跳转通过GOT[4]跳转到函数而不是PLT[2]的下一条地址。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mik0xI1M-1624470134887)(/CSAPP/image075.png)]

通过readelf工具,在hello的节头表中可以发现GOT表,如图5-30所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-35oius8Y-1624470134888)(/CSAPP/image076.png)]

可以看到.got.plt位于0x404000处,如图5-31所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wFWX8QxT-1624470134888)(/CSAPP/image077.png)]

我们可以看到在执行_init之后,发生了变化。如图5-30及5-31所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MTCag0Xm-1624470134888)(/CSAPP/image078.png)]

在之后的函数调用时,首先跳转到PLT执行.plt中逻辑,第一次访问跳转时GOT地址为下一条指令,将函数序号压栈,然后跳转到PLT[0],在PLT[0]中将重定位表地址压栈,然后访问动态链接器,在动态链接器中使用函数序号和重定位表确定函数运行时地址,重写GOT,再将控制传递给目标函数。之后如果对同样函数调用,第一次访问跳转直接跳转到目标函数。

5.8 本章小结

本章详细讨论了链接的概念与作用、在Ubuntu下链接的命令,以及可执行目标文件hello的格式,详细分析可执行目标文件和可重定位目标文件的区别。详细解释了hello虚拟地址空间。细致分析了链接的重定位过程。模拟hello的执行流程,并深入探讨了hello的动态链接。

第六章 hello进程管理

6.1 进程的概念与作用

进程的概念:

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

进程的作用:

在现代系统上运行一个程序时,进程为用户提供一个假象:就好像我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存,处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。

每次用户通过向shell 输入一个可执行目标文件的名字,运行程序时, shell 就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。应用程序也能够创建新进程,并且在这个新进程的上下文中运行它们自己的代码或其他应用程序。

进程提供给应用程序两个关键抽象:一个独立的逻辑控制流;一个私有的地址空间。

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

Shell-bash的作用:Shell是一个命令行解释器,它为用户提供了一个向Linux内核发送请求以便运行程序的界面系统级程序,用户可以用Shell来启动、挂起、停止甚至编写一些程序。Shell还是一个功能相当强大的编程语言,易编写,易调试,灵活性较强。Shell是解释执行的脚本语言,在Shell中可以直接调用Linux系统命令。bash提供了一个图形化界面,提升交互速度。

Shell-bash的处理流程:

  • 从终端或控制台获取用户输入命令
  • 将用户输入命令进行解析,判断输入命令是否为内置命令
  • 若是内置命令,则直接执行;若不是内置命令,则bash在初始子进程上下文中加载和运行它
  • 判断程序的执行状态,若为前台进程则等待进程结束;否则直接将进程放在后台执行,继续等待用户下一次输入。

6.3 hello的fork进程创建过程

在终端中输入./hello 1190200708 熊峰 5,shell首先判断它不是内置命令,于是shell查找当前目录下的可执行文件hello,并将其调入内存,shell将它解释为系统功能函数并交给内核执行。Shell通过pid_t fork(void)函数创建一个子进程,子进程会获得与父进程虚拟地址空间相同的一段数据结构的副本。父进程与子进程最大的不同在于他们分别拥有不同的PID。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LaKBDMkW-1624470134889)(/CSAPP/image079.png)]

6.4 hello的execve过程

execve函数加载并运行可执行目标文件hello,且包含相对应的一个带参数的列表argv和环境变量exenvp,只有当出现错误时,例如找不到hello文件等,execve才会返回-1到调用程序,execve调用成功则不会产生。函数原型为:int exeve(const char *filename, const char *argv[], const char *envp[]).

在shell调用fork函数之后,execve调用驻留在内存中的被称为启动加载器的操作系统代码来执行程序,使用启动加载器,子进程调用execve函数,在当前进程即子进程的上下文中加载新程序hello,这个程序覆盖当前正在执行的进程的所有的地址空间,但是这样的操作并没有创建一个新的进程,新的进程有和原先进程相同的PID,并且他还继承了打开hello调用execve函数之前所有已经打开的文件描述符。新的栈和堆段都会被初始化为零,新的代码和数据段被初始化为可执行文件中的内容。只有一些调用程序头部的信息才可能会在加载的过程中被从可执行磁盘复制到对应的可执行区域的内存。

如图6-2所示,当main开始执行时,一个典型的用户栈组织结构如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KNEm1Mzb-1624470134889)(/CSAPP/image080.png)]

如图6-3所示,linux x86-64运行的内存映像。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kwsuqNUh-1624470134889)(/CSAPP/image081.png)]

6.5 hello的进程执行

在现代系统上运行一个程序时,进程为用户提供一个假象:就好像我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存,处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。这种处理效果的具体实现效果本身就是一个逻辑控制流,它指的是一系列可执行程序的计数器pc的值,这些计数值唯一的定义对应于那些包含在程序的可执行文件目标对象中的可执行指令,或者说它指的是那些包含在程序运行时可以动态通过链接触到可执行程序的共享文件对象的可执行指令。
进程的时间片是指一个进程在执行控制流时候所处的每一个时间段。

处理器通过设置在某个控制寄存器中的一个模式位来限制一个程序可以执行的指令以及它可以访问的地址空间。没有设置模式位时,进程就运行在用户模式中。用户模式下不允许执行特权指令,不允许使用或者访问内核区的代码或者数据。设置模式位时,进程处于内核模式,该进程可以访问系统中的任何内存位置,可以执行指令集中的任何命令。进程从用户模式变为内核模式的唯一方式是使用诸如终端、故障或陷入系统调用这样的异常。异常发生时,控制传递到异常处理程序,处理器从用户模式转到内核模式。
上下文在运行时候的状态这也就是一个进程内核重新开始启动一个被其他进程或对象库所抢占的网络服务器时,该进程所可能需要的一个下文状态。它由通用寄存器、浮点数据寄存器、程序执行计数器、用户栈、状态数据寄存器、内部多核栈和各种应用内核数据结构等各种应用对象的最大值数据寄存器组成。

在调用进程发送sleep之前,hello在当前的用户内核模式下进程继续运行,在内核中进程再次调用当前的sleep之后进程转入用户内核等待休眠模式,内核中所有正在处理等待休眠请求的应用程序主动请求释放当前正在发送处理sleep休眠请求的进程,将当前调用hello的进程自动加入正在执行等待的队列,移除或退出正在内核中执行的进程等待队列。

设置定时器,休眠的时间等于自己设置的时间,当定时器时间到时,发送一个中断信号。内核收到中断信号进行终端处理,hello被重新加入运行队列,等待执行,这时候hello就可以运行在自己的逻辑控制流。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VPPQsbkh-1624470134890)(/CSAPP/image082.png)]

6.6 hello的异常与信号处理

hello的异常:在执行过程中,来自处理器外部I/O设备的信号的结果,例如Crtl-C、Crtl-Z等;或有意的执行指令的结果,如系统调用的等。

产生的信号:SIGINT,SIGSTP,SIGCONT,SIGWINCH

  • Crtl-Z

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tW3cawO1-1624470134890)(/CSAPP/image083.png)]

图6-5为正常执行hello程序。

当按下Crtl-Z的时候,将会给进程发出SIGSTP信号,hello程序被挂起,使用ps查看,如图6-6所示,发现存在hello,直到SIGCONT信号到来:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7AGzN2vi-1624470134891)(/CSAPP/image084.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zXg2MZrn-1624470134891)(/CSAPP/image085.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-37uxnPyi-1624470134892)(/CSAPP/image086.png)]

如图6-8,对挂起的程序,发送SIGKILL信号,强制终止进程。

  • Crtl-C

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YG40hBlz-1624470134892)(/CSAPP/image087.png)]

当按下crtl+c的时候,父进程会接收到SIGINT信号,使hello结束运行,用调用wait等函数回收hello进程,使用ps查看,如图6-7所示,当前后台中已没有hello进程。

  • 在运行过程中乱按键盘

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5sL9sH4l-1624470134892)(/CSAPP/image088.png)]

乱按的内容会在屏幕上显示,但是不影响程序的继续执行,getchar将读入一行输入,并且shell会将之后输入的字符串当作新的指令。

  • pstree

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RA1nOBIy-1624470134893)(/CSAPP/image089.png)]

6.7 本章小结

本章详细介绍了可执行文件hello是如何被加载进内存执行的,其进程如何在进程调度器调度下被CPU执行的,并详细介绍了异常控制流,以及信号控制等。以hello程序为例,深入探索了hello进程的fork、execve过程及执行等过程。

第七章 hello的存储管理

7.1 hello的存储器地址空间

  • 逻辑地址:
    逻辑地址(Logical Address)是指由程序hello产生的与段相关的偏移地址部分(hello.o),它由选择符和偏移量组成。
  • 线性地址:
    线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。具体的格式可以表示为:虚拟地址描述符:偏移量程序。它作为一个逻辑分页地址被输入到一个物理地址变换之间的一个中间层,在分页地址变换机制中需要使用一个线性分页地址描述符作为输入,线性地址可以再经过物理地址的变换以产生一个新的物理分页地址。再不同时启用一个分页地址机制的情况下,线性地址本身就是与虚拟地址同意义的。hello的代码会产生逻辑地址,或者说是(即hello程序)段中的偏移地址,它加上相应段的基地址就生成了一个线性地址。
  • 虚拟地址:
    有时我们也把逻辑地址称为虚拟地址。因为与虚拟内存空间的概念类似,逻辑地址也是与实际物理内存容量无关的,是hello中的虚拟地址。
  • 物理地址:
    物理地址(Physical Address)是指出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。如果启用了分页机制,那么hello的线性地址会使用页目录和页表中的项变换成hello的物理地址;如果没有启用分页机制,那么hello的线性地址就是物理地址。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DX8oqhcF-1624470134893)(/CSAPP/image090.png)]

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

  • 将内存分为不同的段,段有段寄存器对应,段寄存器:CS(代码段):程序代码所在段;SS(段栈):栈区所在段;DS(数据段):全局静态数据区所在段,以及ES、GS、FS等备用寄存器。分段功能在实模式和保护模式下有所不同:
    • 实模式:逻辑地址CS:EA到物理地址CS*16+EA
    • 保护模式:以段描述符作为下标,到GDT/LDT表查表获得段地址,段地址+偏移地址=线性地址
      段选择符的suo因组成和定义如下,分别指的是索引为位(index),TI,RPL,当索引位TI=0时,段描述符在rpgdt中,TI=1时,段描述表在rpldt中。而索引位index就类似于一个数组,每个元素内都存放一个段的描述符,索引位首地址就是我们在查找段描述符时在这个元素数组当中的索引。一个段描述符的首地址是指含有8个元素的字节,我们通常可以查找到段描述符之后获取段的首地址,再把它与线性逻辑地址的偏移量进行相加就可以得到段所需要的一个线性逻辑地址。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-voRFs7zH-1624470134894)(/CSAPP/image091.png)]

如图7-3所示,其中RPL提供权限管理,TI提供描述表选择。高13位则作为地址,给处当前段在段描述符表中的位置。即在整个段表中的偏移。

其后,即可按照一定的流程进行逻辑地址到线性地址转化:

在段选择符中,用TI全段选择附表的类型,在使用高13位的地址,找到段描述符表,获得32位的段基址,再将段内偏移与段基址相加,获得32位线性地址,但是在现代Linux系统中,不适用分段机制,将前16位全设置为0,逻辑地址即线性地址。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ATIMsPNL-1624470134894)(/CSAPP/image092.png)]

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

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

  • 首先Linux系统又自己的虚拟内存系统,Linux将虚拟内存组织成一些段的集合,段之外的虚拟内存不存在因此不需要记录。
  • 内核为hello进程维护了一个段的任务结构即图中的task_struct,其中条目mm->mm_struct(描述了虚拟内存的当前状态),pgd->第一级页表的基地址(结合一个进程一串页表),mmap->vm_area_struct的链表。一个链表条目对应一个段,链表相连指出了hello进程虚拟内存中的所有段。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bMj37TnW-1624470134895)(/CSAPP/image093.png)]

  • 虚拟页(VP):系统将每个段分割为大小固定的块,来作为进行数据传输的单元。对于Linux,每个虚拟页大小为4KB.
  • 物理页(PP/页帧):类似于虚拟页,虚拟内存也被分割。虚拟内存系统中MMU负责地址翻译,MMU使用页表,即存一种放在物理内存中的数据结构,将虚拟页到物理页映射,即虚拟地址到物理地址的映射。
  • 通过页表机制寄存器PTBR+VPN在页表中获得PTE,PTE中包含有效位、权限信息、物理页号。
    • 如果有效位时0+NULL则代表没有在虚拟内存空间中分配该内存;
    • 如果时有效位0+非NULL,则代表在虚拟内存空间中分配了但是没有被缓存到物理内存中;
    • 如果有效位是1,则代表该内存已经缓存在了物理内存中,可以得到其物理页号PPN,与虚拟页偏移量共同构成物理地址PA。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mpNLnitM-1624470134895)(/CSAPP/image094.png)]

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

如果按照上述模式,每次CPU产生一个虚拟地址并且发送给地址管理单元,MMU就必须查找一个PTE行来用将虚拟地址翻译成物理地址。为了消除这种操作带来的大量时间开销,MMU中设计了一个关于PTE的小的缓存,称为翻译后备缓冲器(TLB)也叫快表。例如当每次CPU发现需要重新翻译一个虚拟地址是,它就必须发送一个VPN得到虚拟地址MMU,发送一个VPO位得到一个L1高速缓存,例如当我们使用MMU向一个TLB的组请求一个页表中的条目时,告诉缓存通过一个VPO位在页表中查找一个相应的数据标记组,并在页表中读出这个组里的个数据标记和相应的数据关键字,当MMU从一个TLB的组得到一个PPN时,代表缓存的工作在这个组的请求之前就已经完全准备好,这个组的PPN与就已经可以与这些数据标记文件中的一个虚拟地址进行很好的匹配。

Core i7采用四级页表层次结构,每个四级页表进程都有它自己的私有的页表层次结构,这种设计方法从两个基本方面就是减少了对内存的需求,如果一级页表的PTE全部为空,那么二级页表就不会继续存在,从而为进程节省了大量的内存,而且也只有一级页表才会有需要总是在一个内存中。四级页表的层次结构操作流程如下:36位虛拟地址被寄存器划分出来组成四个9位的片,每个片被寄存器用作到一个页表的偏移量。CR3寄存器内储存了一个L1页表的一个物理起始基地址,指向第一级页表的一个起始和最终位置,这个地址是页表上下文的一部分信息。VPN1提供了到一个L1PTE的偏移量,这个PTE寄存器包含一个L2页表的起始基地址。VPN2提供了到一个L2PTE的偏移量,一共四级,逐级以此层次类推。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-03Av8oaV-1624470134895)(/CSAPP/image095.png)]

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

三级cache的缓存层次的结构如图7-7所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pB4aMzFG-1624470134896)(/CSAPP/image096.png)]

物理地址的结构包括组索引位CI(倒数7-12位),使用它进行组索引,使用组索引位找到对应的组后,假设我们的cache采用8路的块,匹配标记位CT(前40位)如果匹配成功且寻找到的块的有效位valid上的标志的值为1,则命中,根据数据偏移量CO(后6位)取出需要的数据然后进行返回。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LyMADbpB-1624470134896)(/CSAPP/image097.png)]

如果没有数据被匹配成功,或者匹配成功但是标记位是1,这些都是未命中即miss的情况,此时向下一级缓存中查询数据(L2 Cache->L3 Cache->主存),并且将查找到的数据加载到Cache里,我们通常采用LRU策略,确定哪一个块作为牺牲快。

7.6 hello进程fork的内存映射

Shell通过调用fork的函数可以让进程内核自动创建yige新的进程,这个新的进程拥有各自新的数据结构,并且被内核分配了唯一的pid。它有着自己独立的虚拟内存空间。

虚拟内存和内存映射解释了fork函数如何为hello进程提供私有的虚拟地址空间。fork为hello的进程创建虚拟内存,创建当前进程的的mm_struct,vm_area_struct和页表的原样副本;两个进程中的每个页面都标记为只读;两个进程中的每个区域结构都标记为私有的写时复制,在hello进程中返回时,hello进程拥有与调用fork进程相同的虚拟内存。随后的写操作通过写时复制机制创建新页面。

并且它还拥有自己独立的逻辑控制流,它同样可以拥有当前已经可以打开的各类文件信息和页表的原始数据和样本,为了有效保护进程的私有数据和信息,同时为了节省对内存的消耗,进程的每个数据区域都被内核标记起来作为写时复制。

7.7 hello进程execve时的内存映射

在bash中的进程中执行execve的函数调用,execve函数在当前进程中加载并运行包含在可执行文件hello中的程序,用hello替代了当前bash中的程序。

下面是加载并运行hello的几个步骤

  • 删除已存在的用户区域:删除当前进程虚拟地址的用户部分的已存在的区域结构。
  • 映射私有区域:为新程序的代码、数据、bss和栈区域创建新的区域结构,所有这些新的区域都是私有的、写时复制的。
  • 映射共享区域:hello程序与共享对象libc.so链接,libc.so是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
  • 设置程序计数器(PC):execve会设置当前进程上下文的程序计数器,使之指向代码区域的入口点后,对该进程的调度就将重代码区域入口点开始,并根据需要通过缺页故障将磁盘中的数据与代码载入物理内存。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rwNeQCUg-1624470134897)(/CSAPP/image098.png)]

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

如图7-10,处理缺页是由硬件和操作系统内核协作完成的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PffFZISl-1624470134897)(/CSAPP/image099.png)]

具体处理流程如下:

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

7.9 动态存储分配管理

动态内存分配器维护着一个进程的虚拟内存区域,称为堆。系统之间细节不同,但是不失通用性,假设堆是一个请求二进制零的区域,它紧接在未初始化的数据区域后开始,并向上生长。对于每个进程,内核维护着一个变量 brk,它指向堆的顶部。

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

分配器有两种基本风格:显式分配器,隐式分配器。两种分隔偶要求应用显示地分配块。它们的不同之处在于由哪个实体来负责释放已分配的块。

显式分配器:要求应用显式地释放任何已分配的块。例如,c标准库提供一种叫做malloc程序包的显式分配器。c程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块。c++中的new和delete操作符与c中的malloc和free相当。

显示空闲链表:将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。例如,堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个pred(前驱)和succ(后继)指针。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tKyhebin-1624470134898)(/CSAPP/image100.png)]

隐式分配器:另一方面,要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾收集器,而自动释放未使用的已分配的块的过程叫做垃圾收集,例如,注入Lisp、ML以及Java之类的高级语言就依赖垃圾收集来释放已分配的块。

分离适配:分配器维护着一个空闲链表的数组,每个空闲链表是和一个大小类相关联的,并且被组织成某种类型的显式或隐式链表。每个链表包含潜在的大小不同的块。malloc采用的就是分离适配的方法。

7.10 本章小结

本章详细说明了hello的存储器地址空间;深入探讨了Intel逻辑地址到线性地址的变换;并对hello的线性地址到物理地址的变换进行了深入的解析;同时还细致分析了TLB与四级页表支持下的VA到PA的变换;三级Cache支持下的物理内存访问;hello在进程fork和execve的内存映射;以及缺页故障与缺页中断处理;最后详细分析了动态存储分配管理原理及方案。

第八章 hello的I/O管理

8.1 Linux的I/O设备管理方法

8.1.1 设备的模型化:文件

所有的I/O设备都被模型化为文件,甚至内核也被映射为文件。
一个Linux文件就是一个m字节的序列。所有的输入、输出都被认为时对相应文件的读和写来执行。

文件的类型有:

  • 普通文件:包含任何数据,分成文本文件、二进制文件。
  • 目录:包含一组链接的文件。每个连接都将一个文件名映射到一个文件
  • 套接字:用于与另一个进程进行跨网络通信的文件

8.1.2 设备管理:Unix I/O

将I/O设备模型化为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使所有的输入、输出都能以一种统一且一致的方式来执行。

我们可以对文件的操作有:打开关闭操作open和close;读写操作read和write;改变当前文件位置lseek等

8.2 简述Unix I/O接口及其函数

Unix I/O接口:

  • 打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个 I/O 设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息,应用程序只需要记住这个描述符。
  • linux shell 创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0) 、标准输出(描述符为1) 和标准错误(描述符为2) 。头文件< unistd.h> 定义了常量STDIN_FILENO 、STOOUT_FILENO 和STDERR_FILENO, 它们可用来代替显式的描述符值。
  • 改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置 k,初始为 0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行 seek,显式地将改变当前文件位置 k。
  • 读写文件。一个读操作就是从文件复制n>0 个字节到内存,从当前文件位置k 开始,然后将k增加到k+n。给定一个大小为m 字节的文件,当k~m时执行读操作会触发一个称为end-of-file(EOF)的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF 符号”。类似地,写操作就是从内存复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
  • 关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源.

函数:

  • 打开和关闭文件。
    打开文件函数原型:int open(char* filename,int flags,mode_t mode)
    返回值:若成功则为新文件描述符,否则返回-1;
    flags:O_RDONLY(只读),O_WRONLY(只写),O_RDWR(可读写)
    mode:指定新文件的访问权限位。
    关闭文件函数原型:int close(fd)
    返回值:成功返回0,否则为-1
  • 读和写文件
    读文件函数原型:ssize_t read(int fd,void *buf,size_t n)
    返回值:成功则返回读的字节数,若EOF则为0,出错为-1
    描述:从描述符为fd的当前文件位置复制最多n个字节到内存位置buf
    写文件函数原型:ssize_t wirte(int fd,const void *buf,size_t n)
    返回值:成功则返回写的字节数,出错则为-1
    描述:从内存位置 buf 复制至多 n 个字节到描述符为 fd 的当前文件位置

8.3 printf的实现分析

  • printf的函数体
int printf(const char *fmt, ...)
{
int i;
char buf[256];
va_list arg = (va_list)((char*)(&fmt) + 4);i = vsprintf(buf, fmt, arg);write(buf, i);return i;}

va_list arg = (va_list)((char*)(&fmt) + 4);

va_list的定义:typedef char *va_list

这说明它是一个字符指针。其中的: (char*)(&fmt) + 4) 表示的是第一个参数。

C语言中,参数压栈的方向是从右往左。当调用printf函数的适合,先是最右边的参数入栈。fmt是一个指针,这个指针指向第一个const参数(const char *fmt)中的第一个元素。fmt也是个变量,它的位置,是在栈上分配的,它也有地址。 对于一个char *类型的变量,它入栈的是指针,而不是这个char *型变量。

  • vsprintf函数
int vsprintf(char *buf, const char *fmt, va_list args)
{ char* p; char tmp[256]; va_list p_next_arg = args; for (p=buf;*fmt;fmt++) { if (*fmt != '%') { *p++ = *fmt; continue; } fmt++; switch (*fmt) { case 'x': itoa(tmp, *((int*)p_next_arg)); strcpy(p, tmp); p_next_arg += 4; p += strlen(tmp); break; case 's': break; default: break; } } return (p - buf); }

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

  • write
write:mov eax, _NR_writemov ebx, [esp + 4]mov ecx, [esp + 8]int INT_VECTOR_SYS_CALL

发现反汇编语句中的int INT_VECTOR_SYS_CALL,它表示要通过系统来调用sys_call这个函数。

  • sys_call
sys_call:call savepush dword [p_proc_ready]stipush ecxpush ebxcall [sys_call_table + eax * 4]add esp, 4 * 3mov [esi + EAXREG - P_STACKBASE], eaxcliret

显示格式化了的字符串。

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

8.4 getchar的实现分析

异步异常-键盘中断的处理:在用户按键盘时,键盘接口获得一个键盘扫描码,这时会产生一个中断的请求,键盘会中断处理子程序。接受按键扫描码转成ASCII码,保存到系统的键盘缓冲区。getchar函数通过调用read系统函数,通过系统调用读取按键的ASCII码,直到接收到回车才返回这个字符串。getchar函数读取一个字符。

8.5本章小结

本章详细描述了Linux的IO设备的管理方法:设备的模型化和设备管理,进而分析了Unix I/O的接口机器函数,细致分析了printf和getchar函数的具体实现。到这,我们完成了hello的一生的分析。

结论

hello程序是几乎所有程序员的第一个程序,它的代码实现非常简单,但实际在计算机中运行时,非常复杂,他几乎需要计算机上大部分的硬件和软件协同合作。
通过本次大作业,使我对hello的一生有了更深刻的了解:
首先经历cpp预处理器的预处理,将所有宏定义等递归展开到hello.i文件中,消除所有的注释;然后经历ccl编译器的编译,将hello.i编译为汇编代码;随后经过汇编器as的汇编,将hello.s会变为可重定位文件hello.o,且此时是二进制机器指令;最后经过ld链接器与其他必要的可重定位文件、共享库链接,并留下动态链接接口,在加载时动态链接。生成了可执行文件hello。
在shell中输入指令,shell调用fork函数,生成子进程,并由execve函数在当前进程即子进程的上下文中加载新程序hello。CPU为hello分配一个时间片,在这个时间片中,hello程序按顺序执行自己的控制逻辑流。当程序执行需要数据时,就会通过多级存储器层次结构层层访问。hello中的访存操作需要经历逻辑地址到线性地址到物理地址的变换。若此时hello遇到了信号与异常的影响,会触发系统调用异常。
hello在运行时会调用一些函数,比如printf等,这些函数与linux I/O的设备模型化密切相关。最后hello会被shell的父进程回收,内核会回收为其创建的信息。

参考文献

[1] RANDALE.BRYANT, DAVIDR.O‘HALLARON. 深入理解计算机系统[M]. 机械工业出版社, 2011.

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

[3] GCC学习 - 预处理 https://blog.csdn.net/xiaosaizi/article/details/105669070

[4] 逻辑地址、线性地址和物理地址之间的转换https://blog.csdn.net/gdj0001/article/details/80135196

设备的管理方法:设备的模型化和设备管理,进而分析了Unix I/O的接口机器函数,细致分析了printf和getchar函数的具体实现。到这,我们完成了hello的一生的分析。

结论

hello程序是几乎所有程序员的第一个程序,它的代码实现非常简单,但实际在计算机中运行时,非常复杂,他几乎需要计算机上大部分的硬件和软件协同合作。
通过本次大作业,使我对hello的一生有了更深刻的了解:
首先经历cpp预处理器的预处理,将所有宏定义等递归展开到hello.i文件中,消除所有的注释;然后经历ccl编译器的编译,将hello.i编译为汇编代码;随后经过汇编器as的汇编,将hello.s会变为可重定位文件hello.o,且此时是二进制机器指令;最后经过ld链接器与其他必要的可重定位文件、共享库链接,并留下动态链接接口,在加载时动态链接。生成了可执行文件hello。
在shell中输入指令,shell调用fork函数,生成子进程,并由execve函数在当前进程即子进程的上下文中加载新程序hello。CPU为hello分配一个时间片,在这个时间片中,hello程序按顺序执行自己的控制逻辑流。当程序执行需要数据时,就会通过多级存储器层次结构层层访问。hello中的访存操作需要经历逻辑地址到线性地址到物理地址的变换。若此时hello遇到了信号与异常的影响,会触发系统调用异常。
hello在运行时会调用一些函数,比如printf等,这些函数与linux I/O的设备模型化密切相关。最后hello会被shell的父进程回收,内核会回收为其创建的信息。

参考文献

[1] RANDALE.BRYANT, DAVIDR.O‘HALLARON. 深入理解计算机系统[M]. 机械工业出版社, 2011.

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

[3] GCC学习 - 预处理 https://blog.csdn.net/xiaosaizi/article/details/105669070

[4] 逻辑地址、线性地址和物理地址之间的转换https://blog.csdn.net/gdj0001/article/details/80135196

HIT计算机系统大作业-程序人生-Hello’s P2P相关推荐

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

    计算机系统 大作业 题 目 程序人生-Hello's P2P 专 业 计算机类 学 号 1180300223 班 级 1803002 计算机科学与技术学院 2019年12月 摘 要 本文主要介绍了一个 ...

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

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

  3. 【2022】哈工大计算机系统大作业——程序人生Hello’s P2P

    2022哈工大计算机系统大作业--程序人生Hello's P2P 摘要 第1章 概述 1.1 Hello简介 1.2 环境与工具 1.3 中间结果 1.4 本章小结 第2章 预处理 2.1 预处理的概 ...

  4. 2022计算机系统大作业——程序人生-Hello’s P2P

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算机 学    号 120L021716 班    级 2003005 学       生 蔡泽栋 指 导 ...

  5. 哈工大计算机系统大作业——程序人生-Hello’s P2P

    计算机系统 大作业 题          目程序人生-Hello's P2P 专          业 计算机科学与技术 学       号120L022401 班          级 200300 ...

  6. 2021哈工大计算机系统大作业——程序人生-Hello’s P2P

    计算机系统 大作业 题     目 程序人生-Hello's P2P 计算机科学与技术学院 2021年6月 摘  要 本文介绍了hello的整个生命过程.利用gcc,gdb,edb,readelf,H ...

  7. 哈工大 计算机系统大作业 程序人生-Hello’s P2P From Program to Process

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算学部 学    号 120L020512 班    级 2003004 学       生 黄鹏程 指 导 ...

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

    计算机系统 大作业 题 目 程序人生-Hello's P2P 专 业 计算学部 学 号 班 级 学 生 指 导 教 师 计算机科学与技术学院 2022年5月 摘 要 为深入理解计算机系统,本文以hel ...

  9. 哈工大2021春计算机系统大作业 程序人生-Hello’s P2P

          计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算机类 学     号 1190200613 班     级 1903004 学       生 ...

最新文章

  1. react select默认选项_reactjs – 如何为Picker提供默认的“Please select …”选项?
  2. PySCF :基于Python的化学模拟框架
  3. 关于组织参加2020年全国大学生智能汽车竞赛山东赛区比赛的通知
  4. OpenCv 金字塔之上采样与下采样
  5. iOS对UIViewController生命周期和属性方法的解析
  6. [攻防世界 pwn]——guess_num
  7. oracle之单行函数1
  8. Hadoop技术在商业智能BI中的应用
  9. tomcat报404
  10. Android开发4: Notification编程基础、Broadcast的使用及其静态注册、动态注册方式...
  11. 斗鱼直播实时数据爬取
  12. 开源OA的公文编辑器详解:公文格式和基本使用
  13. Word怎么制作流程图
  14. ps 提示暂存满,不能导入文件
  15. 大番薯本地模式怎么使用?大番薯u盘启动盘制作工具本地模式重装系统教程
  16. 哪些短信平台能发国际短信?
  17. 极其简单的 使用IDEA 中 实现springboot 热部署 (spring boot devtools版)
  18. H3C交换机配件RS232配置线(DB9针转RJ45) 1
  19. 前端进阶垫脚石-前端工程化
  20. [论文]基于模型的细长体欠驱动水下机器人输出反馈控制:理论与实验

热门文章

  1. JavaAPI操作Hive
  2. Python正则表达式【转】
  3. 数据清洗中异常值(离群值)的判别和处理方法
  4. SMIL彩信MMS技术学习
  5. 【vultr使用流程笔记】
  6. WIin10——QTP10.0运行mgn-mqt82未能生成lservrc文件
  7. fastunit元素控件不显示的问题
  8. 4.12.4nbsp;约翰bull;梅纳德bull;凯恩斯
  9. js重新渲染或重新加载div
  10. Unity中的异步编程【1】—— Unity与async 、 await