哈尔滨工业大学计算机系统大作业——Hello’s P2P

哈尔滨工业大学计算机系统大作业——Hello’s P2P

题 目 程序人生-Hello’s P2P
专 业 xxxx
学   号 xxxxxxxxxx
班   级 xxxxxxx
学 生 xxx  
指 导 教 师 xxx

计算机科学与技术学院
2022年5月

摘 要

本文旨在研究hello从运行到终止的整个过程和细节,利用gdb、edb等工具,基于ubuntu20.04操作系统,对一个C语言源文件进行预处理、编译、汇编、链接的步骤进行细节上的剖析,对hello程序的进程的管理方式和原理,hello文件存储的管理方式和原理分别进行了分析,再分析了hello的IO管理,最后,hello被回收,结束了它的一生。

关键词:P2P;linux;hello;预处理;编译;汇编;链接

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

目 录

  • 哈尔滨工业大学计算机系统大作业——Hello's P2P
  • 第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.4 本章小结
  • 第4章 汇编
    • 4.1 汇编的概念与作用
    • 4.2 在UBUNTU下汇编的命令
    • 4.3 可重定位目标ELF格式
    • 4.4 HELLO.O的结果解析
    • 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.4.1 TLB快表加速
      • 7.4.2 四级页表加速
    • 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的整个过程。
P2P过程:编写hello.c——>cpp预处理器进行预处理得到中间文件hello.i——>ccl编译器进行编译得到汇编语言文件hello.s——>as汇编器进行汇编得到可重定位目标文件hello.o——>ld链接器进行链接得到可执行目标文件hello。流程图如下:
在这里插入图片描述
020过程:在shell里,输入”./hello”运行文件,OS进程管理为hello进行fork,execve,为其分配了一个进程pid,通过mmap函数映射到虚拟内存,然后加载物理内存,cpu为hello分配时间片执行逻辑控制流。当程序运行结束后,shell的父进程就会回收hello子进程,内核删除hello相关的数据结构,hello的一生以被父进程回收为终点。

1.2 环境与工具

列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
1.2.1 硬件环境
CPU:X64 CPU(R5-4600H);3GHz
内存:16G RAM
硬盘空间:512G SSD

1.2.2 软件环境
Windows10 64位
Vmware 16.1.0
Ubuntu 20.04.4 LTS 64位[7]
gdb
edb 1.3.0
vi/vim/gedit+gcc

1.2.3 开发工具
Linux:Codeblocks 64位,vi/vim/gedit+gcc,edb 1.3.0,Codeblocks 64位,gcc

1.3 中间结果

列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第1张图片

1.4 本章小结

本章概述了hello的P2P和020的过程,以及在操作和分析hello,撰写本论文所用到的软硬件环境和工具。

第2章 预处理

2.1 预处理的概念与作用

概念:预处理, 是做些代码文本的替换工作。处理以#开头的指令,比如拷贝 #include包含的文件代码,#define宏定义的替换,条件编译等,就是为编译做的预备工作的阶段。C 编译系统在对程序进行通常的编译之前,首先进行预处理。
作用:c 提供的预处理功能主要有以下三种:
(1)宏定义。宏定义是用一个标识符来表示一个字符串,这个字符串可以是常量、变量或表达式。在宏调用中将用该字符串代换宏名。
(2)文件包含。文件包含是预处理的一个重要功能,它可用来把多个源文件连接成一个源文件进行编译,结果将生成一个目标文件。
(3)条件编译。条件编译允许只编译源程序中满足条件的程序段,使生成的目标程序较短,从而减少了内存的开销并提高了程序的效率。
(4)使用预处理功能便于程序的修改、阅读、移植和调试,也便于实现模块化程序设计。

2.2 在UBUNTU下预处理的命令

应截图,展示预处理过程!
Ubuntu下预处理的命令 :gcc -E hello.c -o hello.i
预处理过程如图:
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第2张图片

2.3 HELLO的预处理结果解析

我们可以看到hello.i文件中已经对#include,#include,#inlcude等头文件进行了预处理,也就是找到 C编译系统所提供的并存放在指定的子目录下的头文件,用文件内容替换这三条语句。如图:
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第3张图片哈尔滨工业大学计算机系统大作业——Hello’s P2P_第4张图片
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第5张图片
我们再找到main入口,发现内容并没有变化:
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第6张图片

2.4 本章小结

本章概述了预处理的概念和作用,Ubuntu系统下预处理的命令,然后对预处理后的文件hello.i文件进行分析。

第3章 编译

3.1 编译的概念与作用

注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
概念:编译是读取源程序(字符流),对之进行词法、语法和语义的分析,将高级语言指令转换为功能等效的汇编代码。
作用:

  1. 扫描(词法分析):识别单词并分类。词法分析器读入组成源程序的字符流,并将其组成有意义的词素的序列。形如这样的词法单元。
  2. 语法分析:基于词法分析得到的一系列记号,生成语法树。
  3. 语义分析:分析各种语法成分的语义特征。语义分析器使用语法树和符号表中的信息来检查源程序是否和语言定义的语义一致 。它同时收集类型信息,并存放在语法树或符号表中,以便在中间代码生成过程使用。
  4. 生成中间代码:生成一种既接近目标语言,又与具体机器无关的表示,便于代码优化与代码生成。
  5. 优化中间代码:局部优化、循环优化、全局优化等;优化实际上是一个等价变换,变换前后的指令序列完成同样的功能,但在占用的空间上和程序执行的时间上都更省、更有效。
  6. 生成汇编代码。

3.2 在UBUNTU下编译的命令

Ubuntu下编译的命令 :gcc -S hello.i -o hello.s
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第7张图片

3.3 HELLO的编译结果解析

此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出现的属于大作业PPT中P4给出的参考C数据与操作,都应解析。
hello.s文件的内容如下图所示:
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第8张图片
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第9张图片
3.3.1 数据处理
(1) 常量
在这里插入图片描述
第3行代码表示只读数据段
下面以.string开头的第6,8行表示两个字符串,对应了hello.c中的两个printf出来的字符串
(2) 局部变量的定义和赋值
在这里插入图片描述
for循环中有将i初始化为0的操作,发现第31行代码定义局部变量i,把它赋值为0,并且i在栈上的位置为-4(%rbp)。
3.3.2 算术操作
在这里插入图片描述
for循环中有i++操作,由于int类型为四字节,因此51行代码使用addl给i加了1。
3.3.3 关系操作
(1)!=
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第10张图片
if语句的条件判断argc!=4,24行代码中cmpl将agrc和4比较,设置条件码,同时由于argv为int类型占32位字节,故使用了l结尾的cmpl指令。
(2)<
在这里插入图片描述
for语句的条件判断i<8,与(1)同理,53行使用cmpl指令设置条件码。
3.3.5 数组操作
在这里插入图片描述
34行将栈指针传给了rax,35行对将rax加,目的是寻找argv的地址,这是由于下面调用printf了数组argv的一项。
3.3.6 控制转移
(1)
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第11张图片 24行代码是if语句的条件判断, agrc!=4,设置条件码,相等则会跳到.L2,否则不跳转继续向下执行。
(2)
在这里插入图片描述
53行是for语句的条件判断,i<8,若成立就会跳转到.L4执行for循环内的语句,否则就会跳过for循环。

3.3.7 函数操作
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第12张图片
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第13张图片
27,43行:printf
29行:exit
48行:atoi
50行:sleep
55行:getchar

3.4 本章小结

本章介绍了编译的概念和作用,ubuntu中编译的命令,以及分析了编译器产生的汇编语言文件hello.s的内容。

第4章 汇编

4.1 汇编的概念与作用

注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
概念:汇编阶段就是将汇编文件翻译成机器码(二进制文本)
作用:生成可重定向目标文件,即.o文件。

4.2 在UBUNTU下汇编的命令

应截图,展示汇编过程!
Ubuntu下汇编的命令:gcc -c hello.s -o hello.o
如下图
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第14张图片

4.3 可重定位目标ELF格式

分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
生成ELF文件:用命令 readelf -a hello.o > hello.oelf
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第15张图片
分析hello.o的ELF格式:
(1)ELF头:elf头是位于elf文件的头部,里面存储着一些机器和该ELF文件的基本信息。如图:
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第16张图片
(2)节头:包含了文件中出现的各个节的语义,包括节的类型、位置和大小等信息。如下图所示:
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第17张图片
其中:

.text:已编译程序的机器代码。
.rela.text:一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。
.data:已初始化的全局和静态C变量。局部C变量在运行时被保存在栈中,既不出现在.data节中,也不出现在.bss节中。
.bss:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量。
.rodata:只读数据,比如printf语句中的格式串和开关语句的跳转表。 .comment:版本控制信息。
.note.GNU_stack节:标记可执行堆栈。 .eh_frame节:处理异常。
.rela.eh_frame节:.eh_frame节的重定位信息。
.symtab:一个符号表,存放在程序中定义和引用的函数和全局变量的信息。
.strtab:一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部中的节名字。字符串表就是以nu11结尾的字符串的序列。
.shstrtab节:包含节区名称。

(3) 重定位节:重定位部分有.rela.text和.rela.eh_frame。
设每个节为s,节中的重定位条目为r。每个节和每个符号的运行时地址为ADDR(s),每个符号运行时地址ADDR(r.symbol)。
若这个引用是PC相对寻址:那么就计算refaddr = ADDR(s) + r.offset; 相对PC地址为refptr = (unsigned)(ADDR(r.symbol) + r.addend - refaddr)。
若这个引用是绝对寻址:那么就计算
refptr = (unsigned)(ADDR(r.symbol) + r.addend)。
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第18张图片
在上图中,最左边一列是of,最右边的是addend。需要重定位的由:.rodata段中的两个字符串,函数puts,exit,printf,atoi,sleep,sleep,getchar。
在这里插入图片描述
(4)符号表:.symtab,一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。一些程序员错误地认为必须通过-g选项来编译一个程序,才能得到符号表信息。实际上,每个可重定位目标文件在.symtab中都有一张符号表(除非程序员特意用STRIP命令去掉它)。然而,和编译器中的符号表不同,.symtab符号表不包含局部变量的条目。
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第19张图片

4.4 HELLO.O的结果解析

objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
说明机器语言的构成,与汇编语言的映射关系。特别是机器语言中的操作数与汇编语言不一致,特别是分支转移函数调用等。

用命令:objdump -d -r hello.o > hello.objdump
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第20张图片
查看反汇编文件hello.objdump:
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第21张图片
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第22张图片
而之前得到的汇编语言文件hello.s:

哈尔滨工业大学计算机系统大作业——Hello’s P2P_第23张图片
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第24张图片
以上两者所表达的内容含义基本上是一致的,代码的不同之处在于:
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第25张图片

4.5 本章小结

本章概述了汇编的概念和作用,经过汇编器,汇编语言变成了机器语言,对可重定位目标文件ELF文件的内容进行理解和分析,比较了汇编语言文件hello.s和反汇编文件hello.objdump的区别和联系。

第5章 链接

5.1 链接的概念与作用

注意:这儿的链接是指从 hello.o 到hello生成过程。

概念:链接阶段就是将所有的目标文件打包链接成一个可执行文件。包括静态链接和动态链接,以下内容引用自百度百科:
静态链接是由链接器在链接时将库的内容加入到可执行程序中的做法。链接器是一个独立程序,将一个或多个库或目标文件(先前由编译器或汇编器生成)链接到一块生成可执行程序。静态链接是指把要调用的函数或者过程链接到可执行文件中,成为可执行文件的一部分。
动态链接所调用的函数代码并没有被拷贝到应用程序的可执行文件中去,而是仅仅在其中加入了所调用函数的描述信息(往往是一些重定位信息)。仅当应用程序被装入内存开始运行时,在Windows的管理下,才在应用程序与相应的DLL之间建立链接关系。当要执行所调用DLL中的函数时,根据链接产生的重定位信息,Windows才转去执行DLL中相应的函数代码。

作用:链接过程会进行如下操作:
(1) 符号解析:将每个符号的定义和每个符号的引用联系起来。
(2) 重定位:把每个符号定义与存储器中的一个具体位置联系起来,然后修改所有对这些符号的引用,使得它们指向这个存储器的位置,从而重定位这些节。

5.2 在UBUNTU下链接的命令

使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件
用命令: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

如下图所示:
在这里插入图片描述

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

分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。readelf -a hello > hello.elf
(1) hello的ELF头和hello.o的ELF头差不多。如下图:
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第26张图片(2) 节头:
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第27张图片
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第28张图片
(3) 程序头:可执行文件或共享目标文件的程序头表是一个结构数组。 每种结构都描述了系统准备程序执行所需的段或其他信息。如下图:
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第29张图片
(4) 段节:
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第30张图片
(5)Dynamic section:假如一个object文件参与动态的连接,它的程序头表将有一个类型为PT_DYNAMIC的元素。该“段”包含了.dynamic section。如下图:
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第31张图片

(6)重定位节:重定位节由hello.o的ELF文件中的.rela.text和.rela.eh_frame变成了hello中的.rela.dyn和.rela.plt。
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第32张图片
(7)动态符号表:
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第33张图片
(8)符号表:相比于hello.o的ELF文件的符号表,hello多了更多的符号,多了一些产生的库函数和必要的启动函数。
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第34张图片
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第35张图片

5.4 HELLO的虚拟地址空间

使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
Memory Regions:
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第36张图片
由下图可知,虚拟地址空间范围为0x40000-0x400ff0。
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第37张图片
这样我们就可以依据5.3得到的elf文件,根据各个节的地址在Data Dump中寻找对应的部分。比如说.init节位于401000,那么在edb中对应着下图,其余节的寻找也是同理的。
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第38张图片

5.5 链接的重定位过程分析

objdump -d -r hello 分析hello与hello.o的不同,说明链接的过程。
结合hello.o的重定位项目,分析hello中对其怎么重定位的。
用命令objdump -d -r hello > rehello.objdump,如图所示:
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第39张图片
查看结果gedit rehello.objdump
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第40张图片
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第41张图片
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第42张图片
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第43张图片
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第44张图片
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第45张图片
各个节及其所表示的含义:
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第46张图片
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第47张图片

hello与hello.o的不同:
(1) hello和hello.o都有.text节,而且hello有更多的节。例如:.init,.plt, .fini, .plt.sec,
(2) hello的内容比hello.o更丰富,这是因为hello通过连接各种库,得到了很多的函数和数据。增加的函数位于.plt.sec中:puts,printf,getchar,atoi,exit,sleep
(3)hello的call后加的是所调用函数的绝对地址,hello.o的call后加的是所调用函数的相对地址。

5.6 HELLO的执行流程

使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第48张图片

5.7 HELLO的动态链接分析

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

在进行动态链接前,首先进行静态链接,生成部分链接的可执行目标文件hello。此时共享库中的代码和数据没有被合并到hello中。加载hello时,动态链接器对共享目标文件中的相应模块内的代码和数据进行重定位,加载共享库,生成完全链接的可执行目标文件。

动态链接采用了延迟加载的策略,即在调用函数时才进行符号的映射。使用偏移量表GOT+过程链接表PLT实现函数的动态链接。GOT中存放函数目标地址,为每个全局函数创建一个副本函数,并将对函数的调用转换成对副本函数调用。、

在hello.elf文件中,找到.got表的地址为0000000000403ff0:
在这里插入图片描述
用edb打开hello可执行文件,在Data Dump中找到0000000000403ff0地址。
调用前got的内容:
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第49张图片
调用之后,got条目里会存入指向要调用的目标程序的指针:
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第50张图片

5.8 本章小结

本章概述了链接的概念和作用,和在shell中执行链接的命令,分析了hello的ELF文件格式,分析了hello的虚拟地址空间,并以hello为例分析了链接重定位的过程,hello的执行流程和动态链接分析。

第6章 HELLO进程管理

6.1 进程的概念与作用

概念:进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配的基本单位,是操作系统结构的基础。
作用:在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

作用:
(1) Shell 是是用户使用 Linux 的桥梁。
(2) Shell可以合并编程语言以控制进程和文件,以及启动和控制其他程序。
(3) Shell能够减少大量的重复输入和交互操作,能够进行批量的处理和自动化完成维护,减轻管理层的负担。
处理流程:

6.2 简述壳SHELL-BASH的作用与处理流程

处理流程:
(1) Shell首先从命令行中找出特殊字符(元字符),在将元字符翻译成间隔符号。元字符将命令行划分成小块tokens。Shell中的元字符如下所示: SPACE , TAB , NEWLINE , & , ; , ( , ) ,< , > , |
(2) 程序块tokens被处理,检查看他们是否是shell中所引用到的关键字。
(3) 当程序块tokens被确定以后,shell根据aliases文件中的列表来检查命令的第一个单词。如果这个单词出现在aliases表中,执行替换操作并且处理过程回到第一步重新分割程序块tokens。
(4) Shell对~符号进行替换。
(5) Shell对所有前面带有 符 号 的 变 量 进 行 替 换 。 ( 6 ) S h e l l 将 命 令 行 中 的 内 嵌 命 令 表 达 式 替 换 成 命 令 ; 他 们 一 般 都 采 用 符号的变量进行替换。 (6) Shell将命令行中的内嵌命令表达式替换成命令;他们一般都采用 6Shell(command)标记法。
(7) Shell计算采用$(expression)标记的算术表达式。
(8) Shell将命令字符串重新划分为新的块tokens。这次划分的依据是栏位分割符号,称为IFS。缺省的IFS变量包含有:SPACE , TAB 和换行符号。
(9) Shell执行通配符* ? []的替换。
(10) shell把所有待处理的结果中用到的注释删除,并且按照下面的顺序实行命令的检查:

  • 内建的命令
  • shell函数(由用户自己定义的)
  • 可执行的脚本文件(需要寻找文件和PATH路径)
    (11) 在执行前的最后一步是初始化所有的输入输出重定向。
    (12) 最后,执行命令。

6.3 HELLO的FORK进程创建过程

fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程。父进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程(父进程)的所有值都复制到新的新进程(子进程)中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。

6.4 HELLO的EXECVE过程

execve过程发生在fork()之后,在子进程中通过系统调用execve()可以将新程序加载到子进程的内存空间。这个操作会丢弃原来的子进程execve()之后的部分,而子进程的栈、数据会被新进程的相应部分所替换。即除了进程ID之外,这个进程已经与原来的进程没有关系了

6.5 HELLO的进程执行

结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。

进程上下文概念:进程上下文是进程执行活动全过程的静态描述。我们把已执行过的进程指令和数据在相关寄存器与堆栈中的内容称为进程上文,把正在执行的指令和数据在寄存器与堆栈中的内容称为进程正文,把待执行的指令和数据在寄存器与堆栈中的内容称为进程下文。

  • 时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
  • 任务调度: 大部分操作系统(如Windows、Linux)的任务调度是采用时间片轮转的抢占式调度方式,也就是说一个任务执行一小段时间后强制暂停去执行下一个任务,每个任务轮流执行。任务执行的一小段时间叫做时间片,任务正在执行时的状态叫运行状态,任务执行一段时间后强制暂停去执行下一个任务,被暂停的任务就处于就绪状态等待下一个属于它的时间片的到来。这样每个任务都能得到执行,由于CPU的执行效率非常高,时间片非常短,在各个任务之间快速地切换,给人的感觉就是多个任务在“同时进行”,这也就是我们所说的并发(别觉得并发有多高深,它的实现很复杂,但它的概念很简单,就是一句话:多个任务同时执行)。多任务运行过程的示意图如图:

哈尔滨工业大学计算机系统大作业——Hello’s P2P_第51张图片
阐释调度过程:对于一个CPU,它在一段时间不会只执行一个程序,而是会采用并发的执行方式。在执行hello进程时,内核进行上下文切换将当前进程的控制权交给其他进程,将hello进程的上下文保存好后,当定时器到时发送一个中断信号,此时进入内核状态执行中断处理,将hello进程从等待队列中移出重新加入到运行队列,从内核模式切换到执行其他进程的用户模式,从而就实现了上下文的切换。

6.6 HELLO的异常与信号处理

hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。

6.6.1 程序正常运行时:
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第52张图片
6.6.2 按Ctrl-Z:
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第53张图片
ps命令查看进程,我们可以发现hello并没有终止,而是挂起着:
在这里插入图片描述
jobs命令查看当前暂停的进程:
在这里插入图片描述
pstree命令查看进程树:
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第54张图片
(1)fg命令将任务在前台运行:
在这里插入图片描述
接着运行ps,jobs,pstree指令,可以看到hello进程已停止运行:
在这里插入图片描述
在这里插入图片描述
(2)kill命令:
在这里插入图片描述
kill -1 hello 重启暂停的进程:
在这里插入图片描述
Kill -9 hello 杀死进程,即强制结束进程。
在这里插入图片描述
Kill -2 hello 表示结束进程,但并不是强制性的,常用的 “Ctrl+C” 组合键发出就是一个 kill -2 的信号。可以看到运行这条指令以后,hello还存活着,说明这条指令不是强制性关闭进程。
在这里插入图片描述
6.6.3 按Ctrl-C
运行ps指令后,hello直接终止
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第55张图片
6.6.4 乱按
发现乱按的字符对程序运行没有影响,按下的东西会在程序停止后显示在屏幕上,这是因为按下的东西被存放到了stdin中。

哈尔滨工业大学计算机系统大作业——Hello’s P2P_第56张图片

6.7本章小结

本章概述的进程的概念和作用,简述了壳shell——这一用户用来访问操作系统内核的重要应用程序的作用和处理流程,然后对fork和execve的调用进行分析,fork创建新进程,execve执行hello程序,最后分析了hello的异常信号。

第7章 HELLO的存储管理

7.1 HELLO的存储器地址空间

结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。

逻辑地址:指由程序产生的段内偏移地址。逻辑地址与虚拟地址二者之间没有明确的界限。

线性地址:指虚拟地址到物理地址变换的中间层,是处理器可寻址的内存空间(称为线性地址空间)中的地址。程序代码会产生逻辑地址,或者说段中的偏移地址,加上相应段基址就成了一个线性地址。如果启用了分页机制,那么线性地址可以再经过变换产生物理地址。若是没有采用分页机制,那么线性地址就是物理地址。

虚拟地址:是由程序产生的由段选择符和段内偏移地址组成的地址。这2部分组成的地址并不能直接访问物理内存,而是要通过分段地址的变化处理后才会对应到相应的物理内存地址。

物理地址:指内存中物理单元的集合,他是地址转换的最终地址,进程在运行时执行指令和访问数据最后都要通过物理地址来存取主存。

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

段式管理的基本思想 :分区式管理和页式管理时的进程地址空间结构都是线性的,这要求对源地址进程程序进行编译、链接时,把源程序中的主程序、子程序、数据区等按线性空间的一维地址顺序排列起来。这使得不同作业或进程之间共享共有子程序和数据变得非常困难。

再者,从链接的角度看,分区管理和页式管理只能采用静态链接。一个大的进程可能包含上千个程序模块,对他们链接要花费大量CPU时间,而实际执行时间可能只用到其中的一个子集。

综上所述:段式存储管理是基于为用户提供一个方便灵活的程序设计环境而提出来的。段式管理的基本思想是:把程序按内容或过程(函数)关系分成段,每段有自己的名字。一个用户作业或进程所包含的段对应于一个二维线性虚拟空间,也就是一个二维虚拟存储器。段式管理程序以段为单位分配内存,然后通过地址映射机构把逻辑地址转换成实际的线性地址。

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

将程序的逻辑地址空间划分为固定大小的页(page),而物理内存划分为同样大小的页框(page frame)。程序加载时,可将任意一页放人内存中任意一个页框,这些页框不必连续,从而实现了离散分配。该方法需要CPU的硬件支持,来实现逻辑地址和物理地址之间的映射。在页式存储管理方式中地址结构由两部构成,前一部分是页号,后一部分为页内地址w(位移量),如图:
在这里插入图片描述
在系统中设置地址变换机构,能将用户进程地址空间中的逻辑地址变为内存空间中的物理地址。
由于页面和物理块的大小相等,页内偏移地址和块内偏移地址是相同的。无须进行从页内地址到块内地址的转换。
地址变换机构的任务,关键是将逻辑地址中的页号转换为内存中的物理块号。物理块号内的偏移地址就是页内偏移地址。
页表的作用就是从页号到物理块号的转换,所以地址变换的任务借助于页表来完成的

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

7.4.1 TLB快表加速

TLB的全程是翻译后备缓冲器,是一个虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块。如下图所示:
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第57张图片
TLB是我们引入的加速机制。
下图展示了当TLB 命中时(通常情况)所包括的步骤。
第1 步:CPU 产生一个虚拟地址。
第2 步和第3 步:MMU拿着VPN去TLB中找,并从TLB 中取出相应的PTE。注意,这个查询速度是非常快的。因为有很多晶体管并行查询,时间接近于O(1)
第4 步:MMU 将这个虚拟地址翻译成一个物理地址,并且将它发送到高速缓存/主存。
第5 步:高速缓存/主存将所请求的数据字返回给CPU
如下图所示:
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第58张图片
当TLB 不命中时,MMU 必须从L1 缓存中取出相应的PTE, 如下图所示。新取出的PTE 存放在TLB 中,可能会覆盖一个已经存在的条目。
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第59张图片
TLB的引入可以一定程度上解决虚拟地址到物理地址翻译的开销问题,接下来还需要解决另一个问题:大页表。

7.4.2 四级页表加速

图7.9给出了Core i7 MMU 如何使用四级的页表来将虚拟地址翻译成物理地址。36位VPN 被划分成四个9 位的片,每个片被用作到一个页表的偏移量。CR3 寄存器包含Ll页表的物理地址。VPN 1 提供到一个Ll PET 的偏移量,这个PTE 包含L2 页表的基地址。VPN 2 提供到一个L2 PTE 的偏移量,以此类推。
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第60张图片

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

首先我们要知道Page Table也有可能存放在L1 Cache当中的。因此从MMU出来的 PTEA首先会在L1 Cache中查找,找不到就会在L2 Cache中查找,接着在L3 Cache中查找。

  • 如果命中了,那么就从Cache返回PTE到MMU,然后MMU再生成物理地址去Cache中请求数据

    • 如果Cache中有数据,那么Cache会返回数据给CPU

    • 如果Cache中没有数据,那么就会跟着物理地址去内存中查找

  • 如果未命中,那么就跑到主存中的页表当中去查找,然后的处理逻辑和上面所说的相同
    如下图所示:
    哈尔滨工业大学计算机系统大作业——Hello’s P2P_第61张图片

7.6 HELLO进程FORK时的内存映射

当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并给他分配一个唯一的PID。为了给这个进程创建虚拟内存,它创建了mm_struct、区域结构和页表的原样副本。他将两个进程中的每个页面都标记从只读,并将两个进程中的每一个区域结构都标记位私有的写时复制。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间。

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

7.7 HELLO进程EXECVE时的内存映射

加载并运行hello需要以下几个步骤:
(1)删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。

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

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

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

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

缺页故障:处理器生成VA,把它传送给MMU然后MMU生成PTEA,高速缓存/主存 向MMU返回PTE,若发现PTE的有效位是0,说明缺页了。

缺页中断处理:
(1) 接着,发生缺页了,传递到CPU中的控制到操作系统内核中的缺页异常处理程序。
(2) 缺页处理程序确定出物理内存中的牺牲页,如果这个页面已经被修改了,则将其换出到磁盘。(回写)
(3) 缺页处理程序页面从磁盘中调取新的页面到Cache/Main Memory,并更新内存中的PTE。
(4) 缺页处理程序返回到原来的进程,再次执行导致缺页的指令。CPU 将引起缺页的虚拟地址重新发送给MMU。

因为虚拟页面现在缓存在物理内存中,所以就会命中,在MMU 执行了页命中情况的几个步骤之后,主存就会将所请求字返回给处理器。

如下图所示:
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第62张图片

7.9 动态存储分配管理

Printf会调用malloc,请简述动态内存管理的基本方法与策略。

动态内存分配器维护着一个进程的虚拟内存区域,称为堆。对于每个进程,内核维护着一个变量brk,指向堆的顶部。如下图所示:

哈尔滨工业大学计算机系统大作业——Hello’s P2P_第63张图片
分配器将堆视为一组不同大小的块的集合,每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。

分配器有两种基本风格:

• 显式分配器:要求应用显式地释放任何已有的块。C标准库提供称为malloc程序包的显式分配器,C程序通过调用malloc分配内存,通过调用free释放一个块。C++通过new和delete分配和释放。

• 隐式分配器:又称为垃圾收集器。自动释放未使用的已分配的块的过程称为垃圾收集。

动态内存管理的基本方法与策略:

  • 放置已分配的块

应用请求一个k字节的块时,分配器搜索空闲链表,查找一个足够大可以放置所请求快的空闲块。这种搜索方式是由放置策略确定的。常见的策略有首次适配、下一次适配和最佳适配。

  • 分隔空闲块

分配器找到一个匹配的块后,就必须作出一个决策,就是分配这个空闲块中多少空间。一个选择是分配整个空闲块,但这种方式可能造成内部碎片。
如果匹配不太好,分配器通常会选择将这个空闲块分割为两部分,第一部分变成分配块,剩下的变成一个新的空闲块。

  • 获取额外的堆内存

如果不能请求到合适的空闲块,一个解决方法是合并那些在物理内存中相邻的空闲块来创建一个更大的块。如果还是不能生成一个足够大的块,那么分配器会调用sbrk函数,向内核请求额外的堆内存。

  • 合并空闲块

任何分配器都必须合并相邻的空闲块,这个过程称为合并。分配器可以选择立即合并,就是在每次一个块被释放时,就合并所有相邻块。或者也可以选择推迟合并,直到某个分配请求失败,然后扫描整个堆,合并所有的空闲块。
立即合并对于某些请求模式而言,会产生一种形式的抖动,块会反复地合并,然后马上分割。

  • 带边界标记的合并

边界标记技术允许在常数时间内进行对前面块的合并。这种思想是在每个块的结尾处添加一个脚部(footer,边界标记),其中脚部就是头部的一个副本。如果每个块包含一个脚部,那么分配器就可以通过检查它的脚部,判断前面一个块的起始位置和状态,这个脚部总是在距当前块开始位置一个字的距离。

7.10本章小结

本章概述了逻辑地址、物理地址、虚拟地址、线性地址的概念和它们之间的区别与联系,一方面阐述了从逻辑地址变换为线性地址的段式管理,另一方面阐述了从线性地址到物理地址的页式管理,接着介绍了TLB和四级页表的加速作用下的VA到PA的变换,又介绍了多级Cache支持下的物理内存访问,hello进程execve时的内存映射、hello进程fork时的内存映射,分析了缺页故障与缺页中断处理程序,最后分析了动态存储分配。

第8章 HELLO的IO管理

8.1 LINUX的IO设备管理方法

所有的I/O设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备映射为文件的方式,允许Linux内核引出一个简单低级的应用接口,称为Unix I/O。

设备的模型化:文件

设备管理:unix io接口

8.2 简述UNIX IO接口及其函数

打开文件——open():open函数将file那么转换为一个文件描述符并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。

关闭文件——close():当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。

读取文件——read():read函数从描述符为fd 的当前文件位置复制最多n个字节到内存位置buf。返回值-1表示一个错误,而返回值0表示EOF。否则,返回值表示的是实际传送的字节数量。

写入文件——write():write函数从内存位置buf复制至多n个字节到描述符fd的当前文件位置。

8.3 PRINTF的实现分析

https://www.cnblogs.com/pianist/p/3315801.html
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
研究printf的实现, 首先来看看printf函数的函数体。

先来看printf函数的内容:

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

va_list的定义: typedef char va_list

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

i = vsprintf(buf, fmt, arg);

vsprintf返回的是要打印出来的字符串的长度,vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化, 产生格式化输出最后调用write。

在write函数中, 将栈中参数放入寄存器,ecx是字符个数, ebx存放第一个字符地址,int INT_VECTOR_SYS_CALLA代表通过系统调用syscall,

Syscall如下

sys_call:

call save

push dword [p_proc_ready]

sti

push ecx

push ebx

call [sys_call_table + eax * 4]

add esp, 4 * 3

mov [esi + EAXREG - P_STACKBASE], eax

cli

ret

一个call save, 是为了保存中断前进程的状态。

ecx中是要打印出的元素个数, ebx中的是要打印的buf字符数组中的第一个元素,这个函数的功能就是不断的打印出字符, 直到遇到:’\0’ 停止。

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

8.4 GETCHAR的实现分析

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

当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的第一个字符的ascii码,如出错返回-1,且将用户输入的字符回显到屏幕。

如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为后,才等待用户按键。

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

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

当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的第一个字符的ascii码,如出错返回-1,且将用户输入的字符回显到屏幕。

如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为后,才等待用户按键。

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

8.5本章小结

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

结论

用计算机系统的语言,逐条总结hello所经历的过程。

你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。

hello所经历的过程:

  1. 首先程序员编写C语言源程序hello.c。
  2. hello.c经过预处理器(cpp)处理,完成宏定义、文件包含和条件编译,得到预处理文件hello.i。
  3. hello.i经过编译器(cl)处理,对之进行词法、语法和语义的分析,将高级语言指令hello.i转换为功能等效的汇编语言代码hello.s。
  4. hello.s经过汇编器(as)处理,生成可重定位目标程序,即hello.o文件。
  5. hello.s经过链接器(ld)处理,与动态链接库进行链接,生成可执行目标文件,即hello文件。
  6. 为了运行hello文件,用户需要通过shell与linux内核进行交互,shell对输入的命令进行分析和解释,对输入运行命令./hello xxxxxxxx xxxxxx xxx。接着,系统的进程管理就会调用fork,创建hello进程,再调用execve执行该进程。hello在运行过程中可能会遇到很多种类的异常,这些异常和信号是由相应的异常处理程序和信号处理程序来进行处理的。
  7. hello文件是存储在磁盘上的,运行以后,它的数据和指令将会从磁盘加载到内存中,从内存加载到L3、L2、L1级高级缓存中,上一级对于下一级来说就是它的缓存。MMU、TLB、多级页表、cache、DRAM内存、动态内存分配器相互协作,共同完成内存的管理。
  8. Unix I/O使得程序与文件进行交互。
  9. 最终,hello程序运行终值以后,shell就会回收掉hello进程,这样它结束了它看似简单而又神秘瑰丽的一生。

我的深切感悟:
hello程序的执行结果看似非常简单,但是它执行的过程不是说一个简简单单的软件、硬件、函数来实现的,它是在软件、硬件、OS等各部分紧密协同一致才可以顺利地完成的,才能给hello一个简单的创造,和一个简单的终止。由此,我体会到了想要搞清楚计算机系统的理论,那就必须要“兼收并蓄、博采众长”,下至计算机底层硬件,上至抽象层的软件算法,还有位于二者交接面的指令集体系结构,都要进行综合调度。

附件

列出所有的中间产物的文件名,并予以说明起作用。
哈尔滨工业大学计算机系统大作业——Hello’s P2P_第64张图片

参考文献

为完成本次大作业你翻阅的书籍与网站等
[1] Randal E. Bryant , David R. O’Hallaron , 深入理解计算机系统(第三版),机械工业出版社
[2] https://www.cnblogs.com/Yoke-cc/p/13860811.html
[3] https://blog.csdn.net/shiyongraow/article/details/81454995?ops_request_misc=&request_id=&biz_id=102&utm_term=%E7%BC%96%E8%AF%91%E7%9A%84%E4%BD%9C%E7%94%A8&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduweb~default-0-81454995.142v10pc_search_result_control_group,157v4new_style&spm=1018.2226.3001.4187
[4] https://blog.csdn.net/southernriver/article/details/52275601?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522165297940916782246478333%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=165297940916782246478333&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduend~default-2-52275601-null-null.142v10pc_search_result_control_group,157v4new_style&utm_term=%E6%AE%B5%E5%BC%8F%E7%AE%A1%E7%90%86&spm=1018.2226.3001.4187

你可能感兴趣的:(linux)