程序人生-Hello‘s P2P

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7VhcuysH-1652883577225)(media/6adc987e0270ab8ce90ba4fe6207a880.jpeg)]

计算机系统

大作业

题 目 程序人生-Hello’s P2P

专 业 计算机类

学 号 120L021329

班 级 2003011

学 生 蒋世鑫

指 导 教 师 郑贵滨

计算机科学与技术学院

2022年5月

摘 要

本论文将CSAPP课程所学内容通过hello小程序的一生,对我们所学进行全面的梳理与回顾。以hello.c原始程序开始,依次深入研究了编译、链接、加载、运行、终止、回收的过程。我们主要在Ubuntu下进行相关操作,合理运用了Ubuntu下的操作工具,结合《深入理解计算机系统》书中的内容与课上老师的讲授,进行细致的历程分析,目的是把计算机系统整个的体系串联在一起,加深对计算机系统的了解。

**关键词:**hello;计算机系统;计算机体系结构;程序生命周期;底层原理;

**
**

目 录

第1章 概述

1.1 Hello简介

1.1.1Hello的P2P过程

1.1.2Hello的020过程

1.2 环境与工具

1.2.1硬件

1.2.2软件

1.2.3开发与调试工具

1.3 中间结果

1.4 本章小结

第2章 预处理

2.1 预处理的概念与作用

2.1.1概念

2.1.2作用

2.2在Ubuntu下预处理的命令

2.3 Hello的预处理结果解析

2.3.1预处理结果

2.3.2解析

2.4 本章小结

第3章 编译

3.1 编译的概念与作用

3.1.1概念

3.1.2作用:

3.2 在Ubuntu下编译的命令

3.2.1命令

3.2.2截图

3.3 Hello的编译结果解析

3.3.1数据

3.3.2赋值

3.3.3算术操作

3.3.4关系操作

3.3.5数组操作

3.3.7函数操作

3.4 本章小结

第4章 汇编

4.1 汇编的概念与作用

4.1.1概念

4.1.2作用

4.2 在Ubuntu下汇编的命令

4.2.1命令:

4.2.2截图:

4.3 可重定位目标elf格式

4.3.1 ELF header

4.3.2节头目表

4.4 Hello.o的结果解析

4.4.1反汇编指令及截图

4.4.2机器语言的构成

4.4.3与汇编语言的映射关系

4.4.4对照分析

4.5 本章小结

第5章 链接

5.1 链接的概念与作用

5.1.1链接的概念

5.1.2链接的作用

5.2 在Ubuntu下链接的命令

5.2.1命令

5.2.2截图

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

5.3.1ELF 头

5.3.2节头目表

5.3.3程序头表

5.4 hello的虚拟地址空间

5.5 链接的重定位过程分析

5.5.1命令

5.5.2分析

5.6 hello的执行流程

5.7 Hello的动态链接分析

5.8 本章小结

第6章 hello进程管理

6.1 进程的概念与作用

6.1.1概念

6.1.2作用

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

6.2.1作用

6.2.2处理流程

6.3 Hello的fork进程创建过程

6.4 Hello的execve过程

6.5 Hello的进程执行

6.6 hello的异常与信号处理

6.6.1正常运行

6.6.2CTRL+Z

6.6.3CTRL+C

6.6.4回车

6.6.5不断乱按

6.7本章小结

第7章 hello的存储管理

7.1 hello的存储器地址空间

7.1.1逻辑地址

7.1.2线性地址

7.1.3虚拟地址

7.1.4物理地址

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

7.2.1含义

7.2.2过程

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

7.3.1概念

7.3.2过程

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

7.4.1基于Intel Core i7

7.4.2过程

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

7.5.1 L1 dCache 参数

7.5.2过程

7.6 hello进程fork时的内存映射

7.7 hello进程execve时的内存映射

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

7.9动态存储分配管理

7.9.1概念

7.9.2方法与策略

7.10本章小结

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

8.1.1设备的模型化

8.1.2设备管理

8.2 简述Unix IO接口及其函数

8.2.1Unix IO接口

8.2.2Unix IO函数

8.3 printf的实现分析

8.3.1 printf()函数体

8.3.2 vsprintf()

8.3.3 write()

8.4 getchar的实现分析

8.5本章小结

结论

附件

参考文献

第1章 概述

1.1 Hello简介

1.1.1Hello的P2P过程

hello程序的生命周期是从一个高级C语言程序hello.c文件开始的,因为程序员通过这种形式写程序并能够被其他人读懂。但为了在系统上运行hello.c程序,hello.c文件需要被转化为可执行文件。

这种从源文件到目标文件的转化是通过编译器驱动程序完成的。以gcc为代表的编译器读取源程序hello.c并通过四个阶段完成这个转化过程。

(1)预处理阶段

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

(2)编译阶段

编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。该程序包含函数main的含义,每条语句都已一种文本格式描述了一条低级机器语言指令。

(3)汇编阶段

汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成可重定位目标程序(relocatable object program)的格式,并将结果保存在文件hello.o中。hello.o文件是一个二进制文件,所包含的字节就是hello的指令编码。

(4)链接阶段

链接器(ld)负责把hello.o调用的标准C库函数printf对应的printf.o的单独预编译目标文件与hello.o程序合并。最终得到可执行目标文件hello,它需要被加载到内存才能被执行。

在shell中输入命令./hello后,shell调用fork()产生子进程Process,实现了hello.c从Program到Process的转化。

1.1.2Hello的020过程

Hello的020是指hello.c文件“From 0 to 0”,初始时内存中并无hello文件的相关内容,这便是“From 0”。通过在Shell下调用execve函数,系统会将hello文件载入内存,执行相关代码,当程序运行结束后, hello进程被回收,并由内核删除hello相关数据,这即为“to 0”。

1.2 环境与工具

列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。

1.2.1硬件

Intel Core i7

2.30 GHz

16.0 GB RAM

512GB HD Disk

1.2.2软件

Windows10 64位;

VirtualBox6.1;

ubuntu20.04;

1.2.3开发与调试工具

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

1.3 中间结果

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

文件名作用
hello.i预处理后得到的文本文件
hello.s编译后得到的汇编语言文件
hello.o汇编后得到的可重定位目标文件
elf.txt用readelf读取hello.o得到的ELF格式信息
asm.txt反汇编hello.o得到的反汇编文件
hello.elf由hello可执行文件生成的.elf文件
hello.asm反汇编hello可执行文件得到的反汇编文件

1.4 本章小结

本章简要介绍了hello 的P2P,020的具体含义,同时列出了论文研究时采用的具体软硬件环境和中间结果。

(第1章0.5分)

第2章 预处理

2.1 预处理的概念与作用

以下格式自行编排,编辑时删除

2.1.1概念

预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。典型地,由预处理器(preprocessor) 对程序源代码文本进行处理,得到的结果再由编译器核心进一步编译。这个过程并不对程序的源代码进行解析,但它把源代码分割或处理成为特定的单位——(用C/C++的术语来说是)预处理记号(preprocessing token)用来支持语言特性(如C/C++的宏调用)。

最常见的预处理是在C语言和C++语言中。ISO C和ISO C++都规定程序由源代码被翻译分为若干有序的阶段(phase),通常前几个阶段由预处理器实现。预处理中会展开.c文件中以#起始的行,试图解释为预处理指令(preprocessing directive) 。其中ISO C/C++要求支持的格式包括#if/#ifdef/#ifndef/#else/#elif/#endif(条件编译)、#define(宏定义)、#include(源文件包含)、#line(行控制)、#error(错误指令)、#pragma(和实现相关的杂注)以及单独的#(空指令)。预处理指令一般被用来使源代码在不同的执行环境中被方便的修改或者编译。

以hello.c中的"#include<stdio.h>"为例,它会把hello.c这一行的命令告诉预处理器。预处理器会读取系统头文件stdio.h的内容,并把它直接插入程序文本中。得到另一个C程序,通常以.i作为文件扩展名。

2.1.2作用

(1)将源文件中用#include 形式声明的文件复制到新的程序中。比如 hello.c 第 6-8 行中的#include<stdio.h> 等命令告诉预处理器读取系统头文件 stdio.h unistd.h stdlib.h 的内容,并把它直接插入到程序文本中。

(2)用实际值替换用#define 定义的字符串

(3)根据#if 后面的条件决定需要编译的代码

(4)特殊符号,预编译程序可以识别一些特殊的符号, 预编译程序对于在源程序中出现的这些串将用合适的值进行替换。

2.2在Ubuntu下预处理的命令

(1)命令: cpp hello.c > hello.i

gcc -E hello.c -o hello.i

(2)截图:

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

图 1预处理命令

2.3 Hello的预处理结果解析

2.3.1预处理结果

(1)代码从原代码的23行经预处理后变成了3060行;

(2)原代码main函数体未改变,但三个预处理命令#include<stdio.h>、#include<unistd.h>、#include<stdlib.h>和注释被约三千行代码取代;

2.3.2解析

(1)取代预处理命令的三千多行代码是三个头文件stdio.h\unistd.h\stdlib.h的依次展开;

(2)从hello.i程序中可以分析得到具体的展开过程:编译器会首先读取原指令,然后到Ubuntu默认的环境变量中寻找头文件,例如"/usr/include/stdio.h"下找到了头文件stdio.h文件。然后把头文件中的代码复制到原代码中取代原指令;

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

图 2复制的stdio.h代码

(3)若得到的新代码中使用了#define或#include语句,则会继续按(2)中过程不断展开,递归循环直至武#define语句为止;

(4)查阅相关资料后,得知编译器同时会把条件编译的命令进行解析选择编译代码;同时删除空指令、注释和多余空白字符;替换部分值;

(4)最后得到一个包含大量库函数、编译指令和原代码的文件,此文件已不需要外部库。

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

图 3原始代码

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

图 4预处理后的代码

2.4 本章小结

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

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

3.1.1概念

编译器通过语法分析和词法分析,在符合语法规则的条件下,将得到的hello.i文件翻译为ASCII汇编语言文件hello.s。这里编译器把hello.i翻译为hello.s,并以文本的形式描述了一条条低级机器语言指令。编译的大概流程为:

(1)语法分析:编译程序的语法分析器以单词符号作为输入,分析单词符号串是否形成符合语法规则的语法单位,方法分为两种:自上而下分析法和自下而上分析法;

(2)中间代码:源程序的一种内部表示,或称中间语言。中间代码的作用是可使编译程序的结构在逻辑上更为简单明确,特别是可使目标代码的优化比较容易实现中间代码。

(3)代码优化:指对程序进行多种等价变换,使得从变换后的程序出发,能生成更有效的目标代码。

(4)目标代码:生成是编译的最后一个阶段。目标代码生成器把语法分析后或优化后的中间代码变换成目标代码。此处指汇编语言代码,须经过汇编程序汇编后,成为可执行的机器语言代码。

3.1.2作用:

(1)分析单词符号串是否形成符合语法规则;

(2)使编译程序的结构在逻辑上更为简单明确;

(3)生成更有效的目标代码;

(4)得到汇编代码,经汇编后可得到机器语言代码。

3.2 在Ubuntu下编译的命令

3.2.1命令

gcc -S hello.i -o hello.s

3.2.2截图

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

图 5编译命令

3.3 Hello的编译结果解析

3.3.1数据

3.3.1.1常量

(1)整形常量

hello.c中的整形常量包括第13行if语句和17行语句至19行语句里的4、0、8、1、2、3。

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

图 6原代码

在hello.s中,整形常量被直接表示为"$+N"的形式。

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

图 7立即数形式

(2)字符串常量

hello.c中的字符串常量包括第14行语句和第18行语句里的字符串,都作为printf()的参数。

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

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

图 8原代码的字符串

这两个字符串常量在汇编代码均储存在.rodata中,表示为

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

图 9编译后的字符串

其中的英文字符和符号不变,而中文字符均用UTF-8编码表示。

3.3.1.2变量

(1)int

hello.c中的int变量包括函数内部定义的int i和传入main的参数int argc。

函数内部的非静态非全局变量都属于局部变量,编译器会把局部变量存储在寄存器或者栈中。编译器每一次对i和argc的调用和赋值操作都会对相应寄存器和栈空间进行操作。在hello.s中,i和argc分别存在%eax和堆栈中。

以下面两个指令为例,分别表示hello.c语句"argc!=4"和"i=0"。

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

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

图 10int编译代码

(2)数组

hello.c中的数组变量是传入main的参数char * argv[4]。

在这里编译器在对其进行编译后,把首地址存入栈中。在需要对其相关教学操作时,只需通过寄存器间接寻址的方法访问。

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

图 11数组编译代码

3.3.2赋值

在汇编代码中,对变量的赋值通过mov指令实现,根据数据的大小决定使用哪条指令。

在这里hello.c中只涉及一句赋值操作i=0,编译器利用movl实现。

3.3.3算术操作

hello.c第17行的int变量i的算术操作i++,被编译器翻译为对栈上存储值加一的形式。通过addl指令,实现栈上对应地址存储值的加一操作。

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

图 12原代码

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

图 13汇编代码

3.3.4关系操作

在汇编语言中,关系操作会利用cmp比较指令和test测试指令实现,同时会设置条件码。

(1)argc!=4

在hello.c中第13行的argc!=4,在hello.s中被编译器翻译为:把存储在对应地址的argc值与4比较,并根据结果设置条件码;

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

图 14汇编代码

(2)i<8

在hello.c第17行的i<8,在hello.s中被编译器翻译为:把存储在对应地址的i的值与8比较,根据结果设置条件码;

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

图 15汇编代码

3.3.5数组操作

在汇编语言中,对数组的操作主要是通过寄存器间接寻址实现的。

在hello.c第18~19行,对字符串指针数组char *argv[]进行了取值操作。翻译时,编译器首先会利用指令move取出argv[]数组基址的值。因为数组是连续存储的,编译器根据数组元素字节的大小,利用指令addl在基址值上加上对应索引的字节的倍数,得到argv[1]、argv[2]和argv[3]的值。char*数据类型大小为八个字节,则只需加8的倍数即可。

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

图 16汇编代码

3.3.6控制转移

汇编语言中,控制转移通过比较指令cmp和跳转指令j*或条件传送指令实现。

  1. if(argc!=4)

这里由argc!=4关系操作设置的条件码决定je是否跳转。如果argc不等于4则执行if函数体内的操作对应的汇编代码;如果等于4,则跳转至L2.,不执行if函数体内的汇编指令。

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

图 17汇编代码

  1. for(i=0;i<8;i++)

这里由i<8关系操作设置的条件码决定jle是否跳转,如果i小于8则执行循环体内的操作(在.L4);如果大于等于8则不跳转,结束循环继续外部操作。

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

图 18汇编代码

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

3.3.7函数操作

C语言中,调用函数时进行的操作如下:

(1)传递控制:

进行过程 Q 的时候,程序计数器必须设置为 Q 的代码的起始地址,然后在返回时,要把程序计数器设置为 P 中调用 Q 后面那条指令的地址。

(2)传递数据:

P 必须能够向 Q 提供一个或多个参数,Q 必须能够向 P 中返回一个值。参数可以地址也可以为值。默认传参的顺序是%rdi>%rsi>%rdx>%rcx>%r8>%r9>栈。而默认返回的值存在%eax中。

(3)分配和释放内存:

在开始时,Q 可能需要为局部变量分配空间,而在返回前,又必须释放这些空间。

  1. main()函数
  2. 参数传递和返回:传入参数argc和argv[],存在%ecx寄存器和;
  3. 函数调用:被shell利用指令call进行函数调用,用栈保存调用位置的下一条指令;
  4. 函数返回:设置%eax为0并利用ret指令返回或直接调用exit()退出。
  5. 对应汇编代码:

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

图 19main对应汇编代码

  1. printf()函数

(1)参数传递和返回:第一次不传入参数,第二次传入argv[1]和argv[2]的值并保存在寄存器中;

(2)函数调用:被main()利用指令call puts或者call printf和相对寻址调用,并用栈保存调用位置的下一条指令;

(3)函数返回:利用ret指令返回;

(4)对应汇编编码:

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

图 20printf对应汇编代码1

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

图 21printf对应汇编代码2

3.exit()

(1)参数传递和返回:传入整形常量1;

(2)函数调用:在main()内的if函数体内当满足if条件时被main()利用指令call和相对寻址调用,并用栈保存调用位置的下一条指令;

(3)对应汇编代码:

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

图 22exit对应汇编代码

4.atoi()

(1)参数传递和返回:传入argv[3],并把值保存在栈或寄存器中。返回一个整形常量保存在%eax中;

(2)函数调用:在main()函数内利用call指令和相对寻址调用,并用栈保存调用位置的下一条指令;

(3)对应汇编代码:

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

图 23atoi对应汇编代码

5.sleep()

(1)参数传递与返回:传入atoi()的返回值—1个整形变量,并将其保存在栈或寄存器中;

(2)函数调用:在main()函数内利用call指令和相对寻址调用,并用栈保存调用位置的下一条指令;

(3)对应汇编代码:

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

图 24sleep对应汇编代码

6.getchar()

(1)函数调用:在main()函数内利用call指令和相对寻址调用,并用栈保存调用位置的下一条指令;

(2)对应汇编代码:

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

图 25getchar对应汇编代码

3.4 本章小结

本章主要介绍了编译的概念以及过程,编译是将文本文件翻译成汇编语言程序,为后续将其转化为二进制机器码做准备的过程。而编译器所做的工作,就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码表示。

同时通过示例函数表现了.c文件中的代码如何转换成为对应汇编指令。另外还以此例详细介绍了汇编代码如何实现变量、常量、传递参数以及分支和循环等工作。

通过本章的分析我更深刻地理解了C语言的数据与操作,对C语言翻译成汇编语言有了更好的掌握。因为汇编语言的通用性,这也相当于掌握了语言间的一些共性。

(第3章2分)

第4章 汇编

4.1 汇编的概念与作用

4.1.1概念

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

4.1.2作用

把汇编语言指令转化为机器可直接识别执行的二进制代码机器语言形式,使得代码可以真正地被机器理解与执行。

4.2 在Ubuntu下汇编的命令

4.2.1命令:

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

4.2.2截图:

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

图 26汇编指令

4.3 可重定位目标elf格式

利用readelf -all hello.o > elf.txt得到可重定向目标程序的elf格式。

4.3.1 ELF header

ELF头以一个16字节的序列Magic开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。另外ELF头的信息还包括ELF头的大小、目标文件的类型(可重定位、可执行目标还是共享目标)、机器类型(如x86-64)、节头部表的文件偏移,以及节头部表中条目的大小和数量。

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

图 27ELF HEADER

4.3.2节头目表

描述了不同节的位置、大小及类型,其中目标文件的每个节都有一个固定大小的条目(entry)。

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

图 28节头表

其中不同flags对应的意义:

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

图 29标志含义

4.3.3重定位节’.rela.text’

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

在这里的重定位声明的量有:.rodata中的模式串,puts,exit,printf,slepsecs,sleep,getchar函数。

每一个列代表的信息为:

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

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

类型—重定位到的目标的类型;

加数—计算重定位位置的辅助信息。

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

图 30.rela.text

4.3.4重定位节’.rela.eh_frame’

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

图 31.rela.eh_frame

4.3.5符号表

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

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

图 32符号表

4.4 Hello.o的结果解析

4.4.1反汇编指令及截图

Objdump -d -r hello.o > asm.txt

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

图 33反汇编指令

4.4.2机器语言的构成

机器语言是用二进制代码表示的计算机能直接识别和执行的一种机器指令的集合。一条指令就是机器语言的一个语句,它是一组有意义的二进制代码,指令的基本格式如,操作码字段和地址码字段,其中操作码指明了指令的操作性质及功能,地址码则给出了操作数或操作数的地址。

4.4.3与汇编语言的映射关系

汇编指令是机器指令便于记忆的书写格式。每行的操作码都与其后的汇编指令一一对应。

4.4.4对照分析

(1)操作数

反汇编代码中的立即数都表示为十六进制的数,而hello.s文件中的数都是十进制的;

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

图 34操作数

(2)分支转移

在hello.s中,跳转指令的目标地址直接记为.L2,.L3等段名。而在反汇编代码中,跳转的目标为具体的地址,在机器代码中体现为相对偏移地址值,即间接地址寻值。

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

图 35分支转移

(3)函数调用

在hello.s文件中,call之后直接跟着函数名称,而在反汇编代码中,call 的目标地址是当前指令的下一条指令并会在下一行有提示信息。

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

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

图 36函数调用

(4)全局变量访问

在hello.s 文件中,使用段名称+%rip访问 .rodata中的模式串,而在反汇编得到的asm.txt中,使用 0+%rip进行访问。其原因与函数调用类似,rodata 中数据地址在运行时才能确定,故访问时也需要重定位。在汇编成为机器语言时,将操作数设置为全 0 并添加相应的重定位条目。

4.5 本章小结

本章介绍了汇编的概念、作用和结果的分析。在Linux系统下,通过汇编器将汇编语言文件转化为可重定位目标文件。并利用readelf指令得到可重定位文件的ELF格式文件,对该文件进行了详细分析。更重点对可重定位目标文件的反汇编代码与直接得到的汇编代码进行了对比分析。深刻地理解了汇编语言到机器语言实现地转变过程,和这其中为下一步链接做出的准备。

(第4章1分)

第5章 链接

5.1 链接的概念与作用

5.1.1链接的概念

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

5.1.2链接的作用

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

从而极大地简化了编写代码的难度,并提高了容错性。

5.2 在Ubuntu下链接的命令

5.2.1命令

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.2.2截图

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

图 37链接指令

使用ld的链接命令,应截图,展示汇编过程! 注意不只连接hello.o文件

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

分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。

输入命令readelf -a hello > hello.elf,区别于Chapter 4 里面的elf.txt文件。

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

图 38readelf指令

5.3.1ELF 头

hello的ELF头与elf.txt的信息基本相同。以一个16字节的序列Magic开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。另外ELF头的信息还包括ELF头的大小、目标文件的类型(可重定位、可执行目标还是共享目标)、机器类型(如x86-64)、节头部表的文件偏移,以及节头部表中条目的大小和数量。

但相比未链接前的可重定位目标文件的ELF格式,类型改变为可执行文件,程序头大小和节头数量均增加,而且获得了入口点地址。

5.3.2节头目表

与未链接前的可重定位目标文件的ELF格式相比,每个节的语义未改变,但节的数量增多了。

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

图 39节头目表

5.3.3程序头表

链接后,ELF格式的可执行目标文件多了一个程序头表。这是一个结构数组,描述了系统准备执行所需的段和其他信息。

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

图 40程序头表

5.3.4段节表

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

图 41段节头表

5.3.5Dynamic section

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

图 42Dynamic section

5.3.6重定位节

与可重定向目标文件的几乎相同,但增多了条目。

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

图 43重定位节表

5.3.7符号表

与可重定向目标文件相比,多了.dynsym符号(也就是动态链接的符号),同时原.symtab的条目也增多了。

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

图 44符号表

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

图 45符号表2

5.4 hello的虚拟地址空间

根据计算机系统的特性,程序被载入至地址0x400000~0x401000中。在该地址范围内,每个节的地址都与前一节中节对应的 Address 相同。根据edb查看的结果,在地址空间0x400000~0x400fff中存放着与地址空间0x400000~0x401000相同的程序,在0x400fff之后存放的是.dynamic到.shstrtab节的内容。

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

图 46Data Dump

5.5 链接的重定位过程分析

5.5.1命令

Objdump -d -r hello > hello.asm

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

图 47反汇编指令

5.5.2分析

(1)链接增加新函数

链接后增加了调用的库函数的代码。在这里我们可以看到链接后的文件中新加入了调用的exit\printf\sleep\atoi\getchar等函数的代码。都是动态链接器将用到的共享库函数加入到了可执行目标文件中。

(2)增加的节

链接后的可执行目标文件增加了.init\.plt\.finl节。其中.init节包含进程初始化时要执行的程序指令,.plt节包含函数连接表,.fiinl节包含进程终止时要执行的程序指令。

(3)函数调用

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

(4)跳转

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

5.5.3链接过程

(1)空间与地址分配

扫描所有的输入目标文件,获得它们的各个段的长度、属性和位置,并且将这些输入目标文件中的符号表中所有的符号定义和符号引用收集起来,统一放到一个全局符号表。再将它们合并。

(2)符号解析与重定位

使用上一步收集到的所有信息,读取输入目标文件中段的数据、重定位信息,并且进行符号解析与重定位、调整代码中的地址等。

5.6 hello的执行流程

0x401000 <_init>

0x401020 <.plt>

0x401030 <puts@plt>

0x401040 <printf@plt>

0x401050 <getchar@plt>

0x401060 <atoi@plt>

0x401070 <exit@plt>

0x401080 <sleep@plt>

0x4010f0 <_start>

0x401120 <_dl_relocate_static_pie>

0x401125 <main>

0x4011c0 <__libc_csu_init>

0x401230 <__libc_csu_fini>

0x401238 <_fini>

5.7 Hello的动态链接分析

编译器没有办法预测函数的运行时地址,所以需要添加重定位记录,等待动态链接器处理,为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,在GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数,在加载时,动态链接器会重定位GOT中的每个条目,使得它包含目标的正确的绝对地址。而.got与.plt节保存着全局偏移量表GOT,其内容从地址0x401000开始。通过edb查看,在dl_init调用前后发生变化。

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

图 48原.got和.plt

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

图 49调用后

对于变量而言,我们利用代码段和数据段的相对位置不变的原则计算正确地址。对于库函数而言,需要plt、got合作,plt初始存的是一批代码,它们跳转到got所指示的位置,然后调用链接器。初始时got里面存的都是plt的第二条指令,随后链接器修改got,下一次再调用plt时,指向的就是正确的内存地址。plt就能跳转到正确的区域。

5.8 本章小结

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

(第5章1分)

第6章 hello进程管理

6.1 进程的概念与作用

6.1.1概念

进程的经典定义就是一个执行中程序的实例。

6.1.2作用

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

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

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

6.2.1作用

Shell-bash是一个用C语言编写的交互型应用程序,代表用户运行其他程序。Shell 应用程序提供了一个界面,用户可以通过这个界面进行系统的基本操作,访问操作系统内核的服务。它可以调用其他程序,并且获取其处理结果、在多个程序之间传递参数同时也能被其他程序调用。

6.2.2处理流程

  1. 读入键盘输入的命令;
  2. 判断命令是否正确,且将命令行的参数改造为系统调用execve()内部处理所要求的形式;
  3. 终端进程调用fork()来创建子进程,终端进程调用wait()来等待子进程完成;
  4. 当子进程运行时,它调用execve()根据命令字符串所指定的文件到目录中查找可执行目标文件,调入内存并执行这个程序;
  5. 如果命令行末尾有后台命令符号&则终端进程不执行等待系统调用,并立即在屏幕上打印提示符,让用户输入下一条指令;如果末尾没有&则终端进程一致等待子进程的完成。子进程完成且被处理后,向父进程报告,此时终端进程被唤醒,做完必要的判别工作后,再发提示符,让用户输入新命令。

6.3 Hello的fork进程创建过程

打开Shell,输入命令./hello 120L021329 蒋世鑫,带参数地执行生成的可执行文件。

首先,shell带参执行当前目录下的可执行文件hello,shell会通过fork函数创建一个新的运行的子进程hello。子进程获取了与父进程的上下文,包括栈、通用寄存器、程序计数器,环境变量和打开的文件相同的一份副本。子进程与父进程的最大区别是有着跟父进程不一样的PID,子进程可以读取父进程打开的任何文件。当子进程运行结束时,父进程如果仍然存在,则执行对子进程的回收,否则就由init进程回收子进程。

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

图 50fork进程

6.4 Hello的execve过程

调用函数fork创建新的子进程之后,子进程会调用execve函数,在当前进程的上下文中加载并运行一个新程序hello。传入参数列表argv和环境变量列表envp。在当前进程的上下文中加载并运行一新的程序。execve 函数从不返回,只有当出现错误时,execve才会返回到调用程序。

(1)在execve加载了Hello之后,它会调用启动代码。而启动代码设置栈,并将控制传递给新程序的主函数main,之后将删除当前进程的代码和地址空间内的内容并将其初始化,然后通过跳转到程序的第一条指令或入口点来运行hello;

(2)将私有的区域映射进来,包括新程序的代码、数据、bss 和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为 hello 文件中的.text 和.data 区;

(3)然后将公共的区域映射进来,比如hello程序与标准C库libc.so链接,这些对象都是动态链接到hello的,然后在用户虚拟地址空间中的共享区域内;

(4)设置程序计数器(PC)。后面加载器跳转到程序的入口点,即设置PC指向_start 地址。_start函数最终调用hello中的 main 函数。下一次调度这个进程时,它将从这个入口点开始执行。Linux 将根据需要换入代码和数据页面。_start 函数调用系统启动该函数__libc_start_main。它初始化执行环境,调用用户层的 main 函数,处理 main 函数的返回值,并且在需要的时候把控制返回到内核。

这样,便完成了在子进程中的加载。

6.5 Hello的进程执行

  1. 在程序运行时,Shell为hello创建了一个子进程,这个子进程与Shell有独立的逻辑控制流,轮流使用处理器,在宏观上看来就像是在平行运行;
  2. 在hello的运行过程中,若hello进程不被抢占,则正常执行;若被抢占,则进入内核模式,进行上下文切换,转入用户模式,调度其他进程;
  3. 直到当hello调用系统函数sleep时,为了最大化利用处理器资源,sleep函数会向内核请求将hello挂起,并进行上下文切换,进入内核模式切换到其他进程,切换回用户模式运行抢占的进程;
  4. 与此同时,将 hello 进程从运行队列加入等待队列,由用户模式变成内核模式,并开始计时;
  5. 当计时结束时,sleep函数返回,发送中断信号,使得hello进程重新被调度。将其从等待队列中移出,转为用户模式。此时 hello 进程就可以继续执行指令。

6.6 hello的异常与信号处理

6.6.1正常运行

进程hello在连续打印八次提示信息后,getchar()会等输入一个字符后结束进程,之后进程将被回收。

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

图 51正常运行

6.6.2CTRL+Z

(1)异常及信号:

陷阱异常,内核产生并向Shell进程发送SIGSTP信号。

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

图 52程序停止

(2)处理:

Shell会转发SIGTSTP 信号到前台进程组中的每个进程,挂起前台作业,即挂起进程 hello。

(3)命令执行与截图:

由ps和jobs命令查看进程,可发现hello进程被挂起而未被回收。

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

图 53进程信息

pstree可查看进程的分层情况,以树状图的形式显示。

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

图 54进程树

可利用kill终止或杀死指定的进程,如hello。

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

图 55kill停止

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

图 56kill杀死进程

停止的hello进程可用fg指令转至前台运行。

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

图 57前台运行

6.6.3CTRL+C

(1)异常及信号:

陷阱异常,内核产生并向Shell进程发送SIGINT信号。

(2)处理:

Shell会转发SIGINT 信号到前台进程组中的每个进程, 结束并回收前台进程hello。

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

图 58hello终止

6.6.4回车

在程序运行中打印回车,shell会将把回车符缓存到stdin中,除最后一个回车被getchar()使用,其他的回车都会等到hello进程结束打印出来。

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

图 59多次按回车

6.6.5不断乱按

在程序执行过程中处上述动作外其他乱按所造成的输入均缓存到stdin,当getchar的时候读出一个’\n’结尾的字串,自动结束并回收hello进程。之后stdin中的其他字串会当做Shell的命令行输入打印到屏幕上。

6.7本章小结

以下格式自行编排,编辑时删除

本章了解了进程管理的概念与作用。并以hello progress的例子分析研究了fork,execve函数的原理与执行过程,并给出了hello带参执行情况下各种异常与信号处理的结果。使我们熟悉了各种指令、信号的作用以及进程管理的大致流程。

(第6章1分)

第7章 hello的存储管理

7.1 hello的存储器地址空间

7.1.1逻辑地址

逻辑地址(Logical Address)是指由程序产生的与段相关的偏移地址部分,逻辑地址由选择符和偏移量两部分组成。具体而言,是指由程序hello产生的与段相关的偏移地址部分。

7.1.2线性地址

线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。逻辑地址经过段机制转化后为线性地址,其为处理器可寻址空间的地址,用于描述程序的分页信息。具体以hello而言,线性地址标志着 hello 应在内存上哪些具体数据块上运行。

7.1.3虚拟地址

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

7.1.4物理地址

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

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

7.2.1含义

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

在段寄存器中,存放着段选择符,可以通过段选择符来得到对应段首地址。段选择符存放在段寄存器(16 位)。前 13 位是索引号,后面 3 位包含一些硬件细节。

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

图 60段寄存器

其中,索引表示用来确定当前使用的段描述符在描述符表中的位置;TI表示根据TI的值判断选择全局描述符表(TI=0,GDT)或选择局部描述符表(TI=1,LDT);RPL表示判断重要等级。RPL=00,为第0级,位于最高级的内核,RPL=11,为第3级,位于最低级的用户状态。

7.2.2过程

给定一个完整的 48 位逻辑地址。

1、首先看段选择符。若T1是0要转换 GDT 中的段,是1转换 LDT中的段,再根据相应寄存器,得到其地址和大小;

2. 拿出段选择符中前13位,查找到对应段描述符,得到32位段基地址;

3、把32位段基地址和32位段内偏移量相加,就得到了线性地址。

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

7.3.1概念

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

例如,一个32位的机器,线性地址可以达到4G,每页4KB,这样,整个线性地址就被划分为2^20页,称之为页表,该页表中每一项存储的都是物理页的基地址为了能够尽可能的节约内存,CPU在页式内存管理方式中引入了两级的页表结构,这种页式管理方式中,第一级的页表称之为“页目录”,用于存放页表的基地址;第二级才是真正的“页表”用于存放物理内存的基地址。

7.3.2过程

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

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

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

图 61虚拟地址到物理地址

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

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

7.4.1基于Intel Core i7

我们具体分析运行Linux的Intel Core i7。

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

图 62intel CPU

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位。

7.4.2过程

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

图 63intel CPU翻译过程

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中添加条目。

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

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

7.5.1 L1 dCache 参数

L1 Cache的基本参数如下:

1. 8路64组相连

2. 块大小64字节

因为块大小为64字节,所以二进制索引为6位,同时块偏移也应该是6位。而一共64组,则需要6位二进制索引和6位组索引。最后,余下标记位则需要40位。故L1 Cache可被划分为CT(40bit)、CI(6bit)和CO(6bit)。

7.5.2过程

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

7.6 hello进程fork时的内存映射

当fork函数被当前进程hello调用时,内核为新进程hello创建各种数据结构,并分配给它一个唯一的PID,同时为了给这个新进程hello创建虚拟内。

Fork函数创建了当前进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。

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

7.7 hello进程execve时的内存映射

shell fork完hello的新进程之后。会执行execve函数进行上下文切换,加载并运行hello。此过程会经历以下几个步骤:

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

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

  2. 映射私有区域

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

  3. 映射共享区域

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

  4. 设置程序计数器

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

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

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

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

图 64缺页异常的处理

若合法,则会该异常进行处理。

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

7.9动态存储分配管理

7.9.1概念

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

7.9.2方法与策略

(1)带边界标签的隐式空闲链表分配器管理

带边界标记的隐式空闲链表的每个块是由一个字的头部、有效载荷、可能的额外填充以及一个字的尾部组成的。

隐式空闲链表:在隐式空闲链表中,因为空闲块是通过头部中的大小字段隐含地连接着的。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。其中,一个设置了已分配的位而大小为零的终止头部将作为特殊标记的结束块。

当一个应用请求一个k字节的块时,分配器搜索空闲链表,查找一个足够大的可以放置所请求块的空闲块。分配器有三种放置策略:首次适配、下一次适配合最佳适配。分配完后可以分割空闲块减少内部碎片。同时分配器在面对释放一个已分配块时,可以合并空闲块,其中便利用隐式空闲链表的边界标记来进行合并。

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

(2)显示空间链表管理

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

在显式空闲链表中。可以采用后进先出的顺序维护链表,将最新释放的块放置在链表的开始处,也可以采用按照地址顺序来维护链表,其中链表中每个块的地址都小于它的后继地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。

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

7.10本章小结

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

(第7章 2分)

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

8.1.1设备的模型化

文件, 所有的I/O设备都被模型化为文件,甚至内核也被映射为文件。

8.1.2设备管理

unix io接口。将所有的IO设备都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O。

这使得所有的输入和输出都能以一种统一的方式来执行:打开文件、改变当前的文件位置、读写文件、关闭文件。

8.2 简述Unix IO接口及其函数

8.2.1Unix IO接口

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.关闭文件

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

8.2.2Unix IO函数

1. open()函数

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

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

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

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

2. close()函数

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

(2)所需头文件: #include <unistd.h>;

(3)函数原型:int close(int fd);

(4)参数:fd需要关闭文件的描述符;

(5)函数返回值:0成功,-1出错。

3. read()函数

(1)功能描述: 从文件读取数据;

(2)所需头文件: #include <unistd.h>;

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

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

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

4. write()函数

(1)功能描述: 向文件写入数据;

(2)所需头文件: #include <unistd.h>;

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

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

5. lseek()函数

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

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

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

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

(6)返回值:成功:返回当前位移;失败:返回-1。

8.3 printf的实现分析

8.3.1 printf()函数体

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

图 65printf()函数

形参列表中的…是可变形参的一种写法,当传递参数的个数不确定时,用这种方式来表示。而printf()函数内有var_list型变量与vsprintf()/write函数;

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

8.3.2 vsprintf()

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

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

图 66vsprintf

vsprintf函数将所有的参数内容按照格式fmt格式化之后存入buf,然后返回格式化数组的长度。

8.3.3 write()

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

图 67write函数

write(buf, i)是将buf中的i个元素写到终端。

其中的INT_VECTOR_SYS_CALL通系统来调用sys_call这个函数。

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

8.4 getchar的实现分析

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

getchar有一个int型的返回值。当程序调用getchar时,程序就等着用户按键,用户输入的字符被存放在键盘缓冲区中直到用户按回车为止(回车字符也放在缓冲区中)。getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

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

8.5本章小结

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

(第8章1分)

结论

hello程序的一生经历了如下过程:

  1. 预处理

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

  2. 编译

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

  3. 汇编

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

  4. 链接

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

  5. 加载运行

    打开Shell,在其中键入 ./hello 1190200208 李旻翀,终端为其fork新建进程,并通过execve把代码和数据加载入虚拟内存空间,程序开始执行;

  6. 执行指令

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

  7. 访存

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

  8. 动态申请内存

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

  9. 信号处理

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

  10. 终止并被回收

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

计算机系统是一个复杂的工程。我们不能既要关心顶层设计,更要关心底层构造。良好的底层知识是我们成为优秀工程师的基石。

附件

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

文件名作用
hello.i预处理后得到的文本文件
hello.s编译后得到的汇编语言文件
hello.o汇编后得到的可重定位目标文件
hello.elf用readelf读取hello.o得到的ELF格式信息
hello.asm反汇编hello.o得到的反汇编文件
hello2.elf由hello可执行文件生成的.elf文件
hello2.asm反汇编hello可执行文件得到的反汇编文件

参考文献

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

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

https://www.cnblogs.com/pianist/p/3315801.html.

[3] 博客园 printf函数实现的深入剖析

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

结论

hello程序的一生经历了如下过程:

  1. 预处理

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

  2. 编译

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

  3. 汇编

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

  4. 链接

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

  5. 加载运行

    打开Shell,在其中键入 ./hello 1190200208 李旻翀,终端为其fork新建进程,并通过execve把代码和数据加载入虚拟内存空间,程序开始执行;

  6. 执行指令

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

  7. 访存

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

  8. 动态申请内存

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

  9. 信号处理

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

  10. 终止并被回收

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

计算机系统是一个复杂的工程。我们不能既要关心顶层设计,更要关心底层构造。良好的底层知识是我们成为优秀工程师的基石。

附件

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

文件名作用
hello.i预处理后得到的文本文件
hello.s编译后得到的汇编语言文件
hello.o汇编后得到的可重定位目标文件
hello.elf用readelf读取hello.o得到的ELF格式信息
hello.asm反汇编hello.o得到的反汇编文件
hello2.elf由hello可执行文件生成的.elf文件
hello2.asm反汇编hello可执行文件得到的反汇编文件

参考文献

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

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

https://www.cnblogs.com/pianist/p/3315801.html.

[3] 博客园 printf函数实现的深入剖析

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值