HIT 2018 CS:APP大作业 程序人生-Hello’s P2P

本文详细剖析了Hello程序从源代码到可执行文件的全过程,包括预处理、编译、汇编、链接,以及在Linux环境下的进程创建、执行、存储管理和IO管理,揭示了程序运行背后的复杂机制。

第1章 概述
1.1 Hello简介

  1. P2P:Program to Process
    在linux中,hello.c经过cpp的预处理、ccl的编译、as的汇编、ld的链接最终成为可执行目标程序hello,在shell中键入启动命令后,shell为其fork,产生子进程,产生子进程后shell为hello execve,于是hello便从Program(个人理解为程序项目)变为Process(进程),这便是P2P的过程。

2.O2O:Zero-0 to Zero-0
这里的020应该指的是Process在内存中From Zero to Zero。产生子进程后shell为hello execve,映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入 main函数执行目标代码,CPU为运行的hello分配时间片执行逻辑控制流。当程序运行结束后,shell父进程负责回收hello进程,内核删除相关数据结构,这便是020的过程。

1.2 环境与工具
1.2.1 硬件环境
X64 CPU;
2GHz;
2G RAM;
256GHD Disk 以上
1.2.2 软件环境
Windows7 64位以上;
VirtualBox/Vmware 11以上;
Ubuntu 16.04 LTS 64位/优麒麟 64位;
1.2.3 开发工具
Gcc 、 CodeBlocks
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.i hello.c 经过预处理后的代码
hello.s hello.i 经编译后的代码
hello.o hello.i 汇编后得到的可重定位目标文件
hello.o.txt hello.o的反汇编文件,用于查看汇编代码
hello.txt hello的反汇编文件,用于查看汇编代码

1.4 本章小结
本章主要简单介绍了hello的P2P(hello从Program变为Process),020(Process在内存中From Zero to Zero)过程,并列出了本次实验信息、环境、中间结果。
(第1章0.5分)

第2章 预处理
2.1 预处理的概念与作用
概念:
预处理(或称预编译)是指在进行编译的第一遍扫描(词法扫描和语法分析)之前所作的工作。预处理指令指示在程序正式编译前就由编译器进行的操作,可放在程序中任何位置。在C语言中,并没有任何内在的机制来完成如下一些功能:在编译时包含其他源文件、定义宏、根据条件决定编译时是否包含某些代码。要完成这些工作,就需要使用预处理程序。尽管在目前绝大多数编译器都包含了预处理程序,但通常认为它们是独立于编译器的。预处理过程读入源代码,检查包含预处理指令的语句和宏定义,并对源代码进行响应的转换。预处理过程还会删除程序中的注释和多余的空白字符。

作用:
其主要作用如下:
1.将所有的#define删除,并展开所有的宏定义;
2.处理所有的预编译指令,例如:#if,#elif,#else,#endif;
3.处理#include预编译指令,将被包含的文件插入到预编译指令的位置;
4.添加行号信息文件名信息,便于调试;
5.删除所有的注释:// /**/;
6.保留所有的#pragma编译指令,因为在编写程序的时候,我们经常要用到#pragma指令来设定编译器的状态或者是指示编译器完成一些特定的动作。

2.2在Ubuntu下预处理的命令
图 2 - 1
图 2 - 1
在ubuntu的shell下输入“cpp hello.c hello.i”即可得到预处理后的ASCII码中间文件hello.i。
2.3 Hello的预处理结果解析
原始hello.c源文件如图2-2:
在这里插入图片描述
图 2 - 2

与hellp.i文件中内容进行对比可见:
1.所有注释被删除并且在其中添加了行号信息文件名信息。(如图2-3)
在这里插入图片描述
图 2 - 3
2.预处理过程中处理#include预编译指令,将被包含的文件插入到预编译指令的位置。(如图2-3、图2-4、图2-5,由于内容过多所以只截取部分图片)
在这里插入图片描述
图 2 - 4
在这里插入图片描述
图 2 - 5
3.剩余的代码部分hello.i与hello.c大致相同。(如图2-6)
在这里插入图片描述
图 2 - 6
2.4 本章小结
预处理是C语言的一个重要功能,它由预处理程序负责完成。当对一个源文件进行编译时,系统将自动引用预处理程序对源程序中的预处理部分作处理,降低了编译过程的复杂性。
在ubuntu的shell下输入“cpp hello.c hello.i”即可得到预处理后的ASCII码中间文件hello.i。
同时本章中还对hello的预处理结果进行了具体的解析。

第3章 编译
3.1 编译的概念与作用
概念:
编译,是指把用高级程序设计语言书写的源程序,翻译成等价的机器语言格式目标程序的过程。
作用:
编译的基本功能是把源程序(高级语言)翻译成目标程序,分为六个步骤:
1、词法分析
编译器将源码看作一个很长的字符串,首先对它进行从左到右的扫描,然后对其做初步分析,识别出代码中的单词(称作Token),分为基本字、标识符、常数、运算符和界符等,方便编译的后续步骤。
在该过程中如果发现不符合词法规则的token,将做出错处理。
2、语法分析
语法分析是对词法分析得到的单词流,按语法规则做进一步的分析,识别出语法单位,如表达式、短语、子句、句子和程序等,从而形成一颗“语法树”。
在该过程中如果发现不符合语法规则的单词流,将做出错处理。
3、语义分析
经过词法分析和语法分析,程序如果没有错误,就可以按照语义要求对其进行“翻译”,形成被称为“四元式”的中间代码。
4、优化
语义分析生成的中间代码不依赖于实际的硬件,便于优化和移植,针对实际状况做一些等效变换,使程序占用内存更小,执行更快。
5、目标代码生成
根据优化后的中间代码,可生成有效的目标代码。而通常编译器将其翻译为汇编代码,此时还需要将汇编代码经汇编器汇编为目标机器的机器语言。
6、出错处理
编译的各个阶段都有可能发现源码中的错误,尤其是语法分析阶段可能会发现大量的错误,因此编译器需要做出错处理,报告错误类型及错误位置等信息
除此之外,作为一个具有实际应用价值的编译系统,除了基本功能之外,还应具备语法检查、调试措施、修改手段、覆盖处理、目标程序优化、不同语言合用以及人-机联系等重要功能。

3.2 在Ubuntu下编译的命令
在这里插入图片描述
图 3 - 1
在ubuntu的shell下输入“gcc -S hello.i -o hello.s”即可将hello.i文件编译后生成hello.s文件。

3.3 Hello的编译结果解析

3.3.1 数据与赋值
将hello.s与hello.c对照可知:
1.参数*argv[ ]与argc在hello.c文件中(图3-2),在hello.s文件中分别存于寄存器%rsi与%edi中(图3-3):
在这里插入图片描述
图 3 - 2
在这里插入图片描述
图 3 - 3
2.exit调用的常数1,在hello.c文件中(如图3-4),在hello.s文件中,在调用exit函数前将该值赋给%edi(如图3-5):
在这里插入图片描述
图 3 - 4
在这里插入图片描述
图 3 - 5
3.局部变量i,在hello.c文件中(如图3-6,3-7),在hello.s中被赋值为0,即将0存于内存%rbp-4中(如图3-8):
在这里插入图片描述
图 3 - 6
在这里插入图片描述
图 3 - 7
在这里插入图片描述
图 3 - 8

3.3.2 算数操作

  1. 64位加法(如图3-9):
    在这里插入图片描述
    图 3 - 9
    2.32位加法(如图3-10):
    在这里插入图片描述
    图 3 - 10
    3.64位减法(如图3-11):
    在这里插入图片描述
    图 3 - 11

3.3.3 关系操作与控制转移

  1. 对‘!=’关系的判断及接下来的条件分支跳转,hello.c中(如图3-12),hello.s中的关系操作及条件分支跳转(如图3-13):
    在这里插入图片描述
    图 3 - 12
    在这里插入图片描述
    图 3 - 13
    2.for循环语句中的关系判断及条件跳转,在hello.c中(如图3-14),在hello.s(如图3-15):
    在这里插入图片描述
    图 3 - 14
    在这里插入图片描述
    图 3 - 15

3.3.4 数组/指针/结构操作
对*argv[ ]指针数组的操作,在hello.c中(如图3-16,3-17),在hello.s中(如图3-18,3-19)
在这里插入图片描述
图 3 - 16
在这里插入图片描述
图 3 - 17
在这里插入图片描述
图 3 - 18
在这里插入图片描述
图 3 - 19
3.3.5 函数操作
1.调用puts函数(如图3-20)
在这里插入图片描述
图 3 - 20

2.调用exit函数(如图3-21)
在这里插入图片描述
图 3 - 21
3.调用printf函数(如图3-22)
在这里插入图片描述
图 3 - 22
4.调用sleep函数(如图3-23)
在这里插入图片描述
图 3 - 23

5.调用getchat函数(如图3-24)
在这里插入图片描述
图 3 - 24
6.返回(如图3-25)
在这里插入图片描述
图 3 - 25
3.4 本章小结
编译,是指把用高级程序设计语言书写的源程序,翻译成等价的机器语言格式目标程序的过程。其基本功能是把源程序(高级语言)翻译成目标程序。
在ubuntu的shell下输入“gcc -S hello.i -o hello.s”即可将hello.i文件编译后生成hello.s文件。
编译器可以处理C语言的各个数据类型以及各类操作,将其转化为汇编语言。本章中就Hello的编译结果进行了具体详尽的解析。

第4章 汇编
4.1 汇编的概念与作用
概念:
把汇编语言翻译成机器语言的过程称为汇编。
用汇编语言编写的程序,机器不能直接识别,要由一种程序将汇编语言翻译成机器语言,这种起翻译作用的程序叫汇编程序。汇编程序的雏型是在电子离散时序自动计算机 EDSAC上研制成功的。这种系统的特征是用户程序中的指令由单字母指令码、十进制地址和终结字母组成。第一个汇编程序是符号优化汇编程序(SOAP)系统,它是50年代中期为IBM650计算机研制的。这种计算机用磁鼓作存储器,每条指令指出后继指令在磁鼓中的位置。当初研制SOAP系统的动机不是引入汇编语言的符号化特色,而是为了集中解决指令在磁鼓中合理分布的问题,以提高程序的运行效率。IBM704计算机的符号汇编程序(SAP)是汇编程序发展中的一个重要里程碑。此后的汇编程序大都以这一系统为模型,其主要特征未发生本质的变化。随着计算机软件的高速发展和广泛应用,汇编程序又吸收了宏加工程序、高级语言翻译程序等系统的一些优点,相继研制出宏汇编程序、高级汇编程序。
作用:
用汇编语言编写的程序,机器不能直接识别,需要将汇编语言翻译成机器语言,这种起翻译作用的过程叫汇编。

4.2 在Ubuntu下汇编的命令
在这里插入图片描述
图 4 - 1
在ubuntu的shell下输入“gcc hello.s -c -o hello.o”即可将hello.s文件汇编后生成hello.o文件。
4.3 可重定位目标elf格式
ELF格式分析:
ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序。ELF头剩下部分包含帮助链接器语法分析和解释目标文件的信息。
不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的条目。
夹在ELF头和节头部表之间的都是节,一个典型的ELF可重定位目标文件包含这些节:.text 、 .rodata 、 .data 、 .bss 、 .symtab 、 .rel.text 、 rel.data等。

  1. ELF文件头:
    在这里插入图片描述
    图 4 - 2
    2.节头表:
    在这里插入图片描述
    图 4 - 3

3…symtab节:
在这里插入图片描述
图 4 - 4
4.重定位节:
在这里插入图片描述
图 4 - 5
其中,Offset是需要被修改的引用的节偏移;Info交代了需要修改的引用的信息;Type描述了被修改引用的重定位类型,其中主要包括32位PC相对地址的引用(R_X86_64_PC32)与32位绝对地址引用(R_X86_64_32);Addend是一个有符号常数,一些类型的重定位使用它对被修改引用的值做偏移调整。

4.4 Hello.o的结果解析
进行反汇编并将结果输入到hello.txt中。
在这里插入图片描述
图 4 - 6
机器语言的构成是二进制的机器指令序列的集合,机器指令则由操作码和操作数组成。
映射:一条机器指令含有一个单字节的指令指示符,与汇编语言中的指令相对应;可能含有一个单字节的寄存器指示符,与汇编语言中的寄存器相对应;还可能含有一个8字节的常数字,对应汇编语言中的立即数、地址指示符的偏移量、分支和调用命令的目的地址。
1.mov指令:
hello.o文件(图4-7,4-9)与hello.s文件(图4-8,4-10)的对比:
在这里插入图片描述
图 4 - 7
在这里插入图片描述
图 4 - 8
在这里插入图片描述
图 4 - 9
在这里插入图片描述
图 4 - 10
2.算数指令:
在这里插入图片描述
图 4 - 11
在这里插入图片描述
图 4 - 12
3.push指令:
在这里插入图片描述
图 4 - 13
4.ret指令:
在这里插入图片描述
图 4 - 14
5.分支转移指令与函数调用指令:
在这两类指令中,汇编语言所描述的是跳转到的目的地的绝对地址,而机器语言中则是使用PC相对地址。(目的地到下一条指令的相对距离)

在这里插入图片描述
图 4 - 15
在这里插入图片描述
图 4 - 16

4.5 本章小结
把汇编语言翻译成机器语言的过程称为汇编。用汇编语言编写的程序,机器不能直接识别,需要将汇编语言翻译成机器语言,这种起翻译作用的过程叫汇编。
在ubuntu的shell下输入“gcc hello.s -c -o hello.o”即可将hello.s文件汇编后生成hello.o文件。
ELF文件由ELF头、节头部表以及夹在ELF头和节头部表之间的节构成。本章中就可重定位目标的elf格式进行了解析。
机器语言的构成是二进制的机器指令序列的集合,机器指令则由操作码和操作数组成。本章中就hello.o的机器编码与汇编语言对比进行了解析。

第5章 链接
5.1 链接的概念与作用
概念:
链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可执行于编译时;也可以执行于加载时;甚至执行于运行时。
作用:
链接在软件开发中扮演着一个关键的角色,因为它们使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小,更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需要简单地重新编译它,并重新链接应用,而不必重新编译其他文件。
5.2 在Ubuntu下链接的命令
在这里插入图片描述
图 5 - 1

在ubuntu的shell下输入“ld -plugin /usr/lib/gcc/x86_64-linux-gnu/5/liblto_plugin.so -plugin-opt=/usr/lib/gcc/x86_64-linux-gnu/5/lto-wrapper -plugin-opt=-fresolution=/tmp/ccBlvjZZ.res -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s --sysroot=/ --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed -dynamic-linker /lib64/ld-linux-x86-64.so.2 -z relro -o hello /usr/lib/gcc/x86_64-linux-gnu/5/…/…/…/x86_64-linux-gnu/crt1.o /usr/lib/gcc/x86_64-linux-gnu/5/…/…/…/x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/5/crtbegin.o -L/usr/lib/gcc/x86_64-linux-gnu/5 -L/usr/lib/gcc/x86_64-linux-gnu/5/…/…/…/x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/5/…/…/…/…/lib -L/lib/x86_64-linux-gnu -L/lib/…/lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/…/lib -L/usr/lib/gcc/x86_64-linux-gnu/5/…/…/… hello.o -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/lib/gcc/x86_64-linux-gnu/5/crtend.o /usr/lib/gcc/x86_64-linux-gnu/5/…/…/…/x86_64-linux-gnu/crtn.o”

5.3 可执行目标文件hello的格式
在这里插入图片描述
图 5 - 2
在这里插入图片描述
图 5 - 3
可执行目标文件的格式类似于可重定位目标文件的格式。ELF头描述文件的总体格式。它还包括程序的入口点,也就是当程序运行时要执行的第一条指令的地址。.text、.data、和.rodata节与可重定位目标文件中的节是相似的,除了这些节已经被重定位到它们最终的运行时内存地址外。.init节定义了一个小函数,叫做_init,程序的初始化代码会调用它。因为可执行文件是完全链接的,所以它不再需要.rel节。
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,和5.3中的对应关系(如图5-4 、 5-5 、5-6):
在这里插入图片描述
图 5 - 4
在这里插入图片描述
图 5 - 5

在这里插入图片描述

图 5 - 6

5.5 链接的重定位过程分析
hello与hello.o反汇编文件的对比:
1.hello.o中的从0开始的地址到了hello中变成了虚拟内存地址:
在这里插入图片描述
图 5 - 7

2.hello相对hello.o多了.init , .plt之类的节:
在这里插入图片描述
图 5 - 8

3.hello中相对hello.o增加了许多的外部链接来的函数:
在这里插入图片描述
图 5 - 9
4.hello.o中跳转以及函数调用的相对偏移地址在hello中都被更换成了虚拟内存地址:
在这里插入图片描述
图 5 - 10

重定位:链接器在完成符号解析以后,就把代码中的每个符号引用和正好一个符号定义(即它的一个输入目标模块中的一个符号表条目)关联起来。此时,链接器就知道它的输入目标模块中的代码节和数据节的确切大小。然后就可以开始重定位步骤了,在这个步骤中,将合并输入模块,并为每个符号分配运行时的地址。在hello到hello.o中,首先是重定位节和符号定义,链接器将所有输入到hello中相同类型的节合并为同一类型的新的聚合节。例如,来自所有的输入模块的.data节被全部合并成一个节,这个节成为hello的.data节。然后,链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每一个符号。当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。然后是重定位节中的符号引用,链接器会修改hello中的代码节和数据节中对每一个符号的引用,使得他们指向正确的运行地址。

5.6 hello的执行流程
Breakpoint 1 at 0x400510
<function, no debug info> _init;
Breakpoint 2 at 0x400540
<function, no debug info> puts@plt;
Breakpoint 3 at 0x400550
<function, no debug info> __libc_start_main@plt;
Breakpoint 4 at 0x400560
<function, no debug info> _IO_getc@plt;
Breakpoint 5 at 0x400570
<function, no debug info> __printf_chk@plt;
Breakpoint 6 at 0x400580
<function, no debug info> exit@plt;
Breakpoint 7 at 0x400590
<function, no debug info> sleep@plt;
Breakpoint 8 at 0x4005b0
<function, no debug info> _start;
Breakpoint 9 at 0x4005e0
<function, no debug info> deregister_tm_clones;
Breakpoint 10 at 0x400620
<function, no debug info> register_tm_clones;
Breakpoint 11 at 0x400660
<function, no debug info> __do_global_dtors_aux;
Breakpoint 12 at 0x400680
<function, no debug info> frame_dummy;
Breakpoint 13 at 0x4006a6
<function, no debug info> main;
Breakpoint 14 at 0x400720
<function, no debug info> __libc_csu_init;
Breakpoint 15 at 0x400790
<function, no debug info> __libc_csu_fini;
Breakpoint 16 at 0x400794
<function, no debug info> _fini;
(注:使用edb单步调试时间太久,故此处我使用gdb来记录的。)

5.7 Hello的动态链接分析
对于动态共享链接库中的PIC函数,编译器无法预测函数的运行时地址,所以需要添加重定位记录,等待动态链接器处理,为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。
dl_init调用前的.got.plt:
在这里插入图片描述
图 5 - 11
dl_init调用后的.got.plt:
在这里插入图片描述
图 5 - 12
其中GOT[1]指向重定位表(依次为.plt节需要重定位的函数的运行时地址)用来确定调用的函数地址,GOT[2]指向动态链接器ld-linux.so运行时地址。
在之后的函数调用时,首先跳转到PLT执行.plt中逻辑,第一次访问跳转时GOT地址为下一条指令,将函数序号压栈,然后跳转到PLT[0],在PLT[0]中将重定位表地址压栈,然后访问动态链接器,在动态链接器中使用函数序号和重定位表确定函数运行时地址,重写GOT,再将控制传递给目标函数。之后如果对同样函数调用,第一次访问跳转直接跳转到目标函数。

5.8 本章小结
链接是将各种代码和数据片段收集并组合成一个单一文件的过程。
链接在软件开发中扮演着一个关键的角色,因为它们使得分离编译成为可能。
并且在本章中还对hello的可执行文件格式、hello的虚拟地址空间、重定位过程、执行流程、动态链接进行了分析。(特别是在资料有限的情况下自学习得了edb的使用方法!!!)

第6章 hello进程管理
6.1 进程的概念与作用
概念:
进程指程序的一次运行过程。更确切地说,进程是具有独立功能的一个程序关于某个数据集合的一次运行活动,因而进程具有动态含义。
作用:
进程为应用程序提供了两个关键抽象:
(1)独立的逻辑控制流:
每个进程拥有一个独立的逻辑控制流,使得程序员以为自己的程序在执 行过程中独占使用处理器。
(2)独立的虚拟地址空间:
每个进程拥有一个私有的虚拟地址空间,使得程序员以为自己的程序在执行过程中独占使用存储器。
6.2 简述壳Shell-bash的作用与处理流程
作用:
在这里插入图片描述
图 6 - 1
处理流程:
(1).读取输入的命令行。
(2).解析引用并分割命令行为各个单词,各单词称为token。其中重定向所在的token会被保存下来,直到扩展步骤(5)结束后才进行相关处理,如进行扩展、截断文件等。
(3).检查命令行结构。主要检查是否有命令列表、是否有shell编程结构的命令,如if判断命令、循环结构的for/while/select/until,这些命令属于保留关键字,需要特殊处理。
(4).对第一个token进行别名扩展。如果检查出它是别名,则扩展后回到(2)再次进行token分解过程。如果检查出它是函数,则执行函数体中的复合命令。如果它既是别名,又是函数(即命令别名和函数同名称的情况),则优先执行别名。在概念上,别名的临时性最强,优先级最高。
(5).进行各种扩展。扩展顺序为:大括号扩展;波浪号扩展;参数、变量和命令替换、算术扩展(如果系统支持,此步还进行进程替换);单词拆分;文件名扩展。
(6).引号去除。经过上面的过程,该扩展的都扩展了,不需要的引号在此步就可以去掉了。
(7).搜索和执行命令。
(8).返回退出状态码。
6.3 Hello的fork进程创建过程
shell中构造完argv与envp后,调用fork()函数,创建一个子进程,与父进程shell完全相同(只读/共享),包括只读代码段,可读写数据段、堆以及用户栈等。
6.4 Hello的execve过程
shell调用execve()函数,在当前进程(fork创建的子进程)的上下文中加载并允许hello程序。将hello中的.text节 、 .data节 、 .bss节等内容加载到当前进程的虚拟地址空间。
6.5 Hello的进程执行
在这里插入图片描述

图 6 - 2
execve函数将hello中的.text节 、 .data节 、 .bss节等内容加载到当前进程的虚拟地址空间后,加载器跳转到hello程序的入口点,开始执行hello进程。在hello进程执行过程中的某些时刻,可能会执行exit(1)或sleep()系统调用。(以sleep函数调用为例)
sleep系统调用,显式地请求让调用进程休眠,从而可能会触发内核进行调度。sleep函数会让调用进程休眠较长的一段时间,所以内核执行从hello进程到某个进程B的上下文切换。在切换进程之前,内核正代表hello进程在用户模式下执行指令。在切换的第一部分中,内核代表hello进程在用户模式下执行指令。然后再某一时刻,内核保存hello进程的上下文,加载进程B及其上下文,内核开始代表进程B在用户模式下执行指令。
进程B在用户模式下运行一段时间后,内核判断sleep函数调用已经终止,就执行进程B到hello进程的上下文切换,操作系统恢复hello进程的上下文,将控制返回给hello进程中紧随在系统调用sleep后的那条指令。hello进程继续运行,直到下一次异常发生,以此类推。

6.6 hello的异常与信号处理
hello中可能出现的异常及其处理方法:
1.陷阱和系统调用:hello中存在exit、sleep等系统调用。当hello要请求上述服务时,将会执行“syscall n”指令,执行syscall指令会导致一个到异常处理程序的陷阱,这个处理程序解析参数,并调用适当的内核程序。(如图6-3所示)
在这里插入图片描述
图 6 - 3
2.故障:hello程序中可能会出现缺页故障等故障,当故障发生时,处理器将控制转移给故障处理程序。如果处理程序能够修正这个错误情况,它就将控制返回到引起故障的指令,从而重新执行它。否则,处理程序返回到内核中的abort例程,abort例程会终止引起故障的应用程序。(如图6-4所示)
在这里插入图片描述
图 6 - 4
hello中可能产生的信号及其处理方法:
hello中可能会出现SIGINT、SIGQUIT、SIGKILL、SIGCHLD等信号。(Ctrl-Z,Ctrl-C以及Ctrl-z后可以运行ps jobs pstree fg kill 等命令可能会导致的)
当hello进程被内核强迫以某种方式对信号的发送做出反应时,它就接受了信号。进程可以忽略这个信号,终止或者通过执行一个称为信号处理程序的用户层函数捕捉这个信号。(如图6-5)
在这里插入图片描述
图 6 - 5
命令截屏:
Ctrl-C:
在这里插入图片描述
图 6 - 6

Ctrl-Z:
在这里插入图片描述
图 6 - 7
ps:
在这里插入图片描述
图 6 - 8
jobs:
在这里插入图片描述
图 6 - 9

pstree:
在这里插入图片描述
图 6 - 10

fg:
在这里插入图片描述
图 6 - 11
kill:
在这里插入图片描述
图 6 - 12

6.7本章小结
进程是具有独立功能的一个程序关于某个数据集合的一次运行活动,因而进程具有动态含义。
进程为应用程序提供了两个关键抽象。
Shell-bash作为命令解释器,有解释命令的功能以及其特殊的处理流程。
shell中输入运行hello的命令行后,hello在shell创建的fork子进程中由execve函数加载后执行。并且其还有自身独特的前后文以及异常和信号处理。

第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址是在有地址变换功能的计算机中,访内指令给出的地址 (操作数) 叫逻辑地址,也叫相对地址,也就是是机器语言指令中,用来指定一个操作数或是一条指令的地址。要经过寻址方式的计算或变换才得到内存储器中的实际有效地址即物理地址。一个逻辑地址由两部份组成,段标识符: 段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是个索引号,后面3位包含一些硬件细节 。
物理地址,CPU地址总线传来的地址,由硬件电路控制(现在这些硬件是可编程的了)其具体含义。物理地址中很大一部分是留给内存条中的内存的,但也常被映射到其他存储器上(如显存、BIOS等)。在没有使用虚拟存储器的机器上,虚拟地址被直接送到内存总线上,使具有相同地址的物理存储器被读写;而在使用了虚拟存储器的情况下,虚拟地址不是被直接送到内存地址总线上,而是送到存储器管理单元MMU,把虚拟地址映射为物理地址。
线性地址也叫虚拟地址,是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址就是线性地址。是一个32位无符号整数,可以用来表示高达4GB的地址,也就是,高达4294967296个内存单元。线性地址通常用十六进制数字表示,值得范围从0x00000000到0xfffffff)程序代码会产生逻辑地址,通过逻辑地址变换就可以生成一个线性地址。如果启用了分页机制,那么线性地址可以再经过变换以产生一个物理地址。如果没有启用分页机制,那么线性地址直接就是物理地址。

7.2 Intel逻辑地址到线性地址的变换-段式管理
知识背景:
1、逻辑地址=段选择符+偏移量
2、每个段选择符大小为16位,段描述符为8字节(注意单位)。
3、GDT为全局描述符表,LDT为局部描述符表。
4、段描述符存放在描述符表中,也就是GDT或LDT中。
5、段首地址存放在段描述符中。
每个段的首地址都存放在自己的段描述符中,而所有的段描述符都存放在一个描述符表中(描述符表分为全局描述符表GDT和局部描述符表LDT)。而要想找到某个段的描述符必须通过段选择符才能找到。图7-1是一个段选择符的格式:
在这里插入图片描述
图 7 - 1
从上图可以看出段选择符由三个部分组成,从右向左依次是RPL、TI、index(索引)。RPL在此不做介绍。先来看TI,当TI=0时,表示段描述符在GDT中,当TI=1时表示段描述符在LDT中。
再来看一下index部分。我们可以将描述符表看成是一个数组,每个元素都存放一个段描述符,那index就表示某个段描述符在数组中的索引。
现在我们假设有一个段的段选择符,它的TI=0,index=8。我们可以知道这个段的描述符是在GDT数组中,并且他的在数组中的索引是8。
假设GDT的起始位置是0x00020000,而一个段描述符的大小是8个字节,由此我们可以计算出段描述符所在的地址:0x00020000+8*index,从而我们就可以找到我们想要的段描述符,从而获取某个段的首地址,然后再将从段描述符中获取到的首地址与逻辑地址的偏移量相加就得到了线性地址。
在这里插入图片描述
`图 7 - 2
7.3 Hello的线性地址到物理地址的变换-页式管理
分页是CPU提供的一种机制,Linux只是根据这种机制的规则,利用它实现了内存管理。在保护模式下,控制寄存器CR0的最高位PG位控制着分页管理机制是否生效,如果PG=1,分页机制生效,需通过页表查找才能把线性地址转换物理地址。如果PG=0,则分页机制无效,线性地址就直接做为物理地址。分页的基本原理是把内存划分成大小固定的若干单元,每个单元称为一页(page),每页包含4k字节的地址空间(为简化分析,我们不考虑扩展分页的情况)。这样每一页的起始地址都是4k字节对齐的。为了能转换成物理地址,我们需要给CPU提供当前任务的线性地址转物理地址的查找表,即页表(page table)。注意,为了实现每个任务的平坦的虚拟内存,每个任务都有自己的页目录表和页表。为了节约页表占用的内存空间,x86将线性地址通过页目录表和页表两级查找转换成物理地址。32位的线性地址被分成3个部分:最高10位 Directory 页目录表偏移量,中间10位 Table是页表偏移量,最低12位Offset是物理页内的字节偏移量。页目录表的大小为4k(刚好是一个页的大小),包含1024项,每个项4字节(32位),项目里存储的内容就是页表的物理地址。如果页目录表中的页表尚未分配,则物理地址填0。页表的大小也是4k,同样包含1024项,每个项4字节,内容为最终物理页的物理内存起始地址。

7.4 TLB与四级页表支持下的VA到PA的变换
图7-3给出了Core i7 MMU 如何使用四级的页表来将虚拟地址翻译成物理地址。36位VPN 被划分成四个9 位的片,每个片被用作到一个页表的偏移量。CR3 寄存器包含Ll页表的物理地址。VPN 1 提供到一个Ll PET 的偏移量,这个PTE 包含L2 页表的基地址。VPN 2 提供到一个L2 PTE 的偏移量,以此类推。
在这里插入图片描述
图 7 - 3
7.5 三级Cache支持下的物理内存访问
本人计算机各级Cache参数:
第一级Cache: C = 32768; S = 64 , s = 6; B = 64 , b = 6; E = 8 , e = 3.
第二级Cache: C = 32768; S = 64, s = 6; B = 64 , b = 6; E = 8 , e = 3.
第三级Cache: C = 3145728; S = 4096, s = 12; B = 64 , b = 6; E =12, e = 4.
在这里插入图片描述
图 7 - 4

在这里我们只讨论L1 Cache的寻址细节,L2 Cache与L3 Cache原理相同。
得到物理地址VA后,MMU发送物理地址给L1 Cache缓存,使用CI进行组索引,再根据CT进行匹配。如果匹配成功且块的valid标志位为1。
如果没有匹配成功或者匹配成功但是标志位不是1,则不命中(miss),向下一级缓存中查询数据(L1 Cache -> L2 Cache -> L3 Cache -> 主存)。查询到数据之后,一种简单的放置策略如下:如果映射到的组内有空闲块,则直接放置,否则组内都是有效块,产生冲突(evict),则采用最近最少使用策略LFU进行替换。
在这里插入图片描述
图 7 - 5
在这里插入图片描述
图 7 - 6

7.6 hello进程fork时的内存映射
当fork 函数被shell调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID 。为了给hello进程创建虚拟内存,它创建了hello进程的mm_struct 、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。
当fork 在hello进程中返回时,hello进程现在的虚拟内存刚好和调用fork 时存在的虚拟内存相同。当这两个进程中的任一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。

7.7 hello进程execve时的内存映射
execve函数在shell的由fork创建的子进程中加载并运行包含在可执行目标文件hello中的程序,用hello程序有效替代了原子进程。加载并运行hello需要以下几个步骤:
1.删除已存在的用户区域。删除shell虚拟地址的用户部分中的已存在的区域结构。
2.映射私有区域。为hello的代码、数据、bss 和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello 文件中的.text和.data 区。bss 区域是请求二进制零的,映射到匿名文件,其大小包含在hello 中。栈和堆区域也是请求二进制零的,初始长度为零。图7.7 概括了私有区域的不同映射。
3.映射共享区域。如果hello程序与共享对象(或目标)链接,比如标准C 库libc. so, 那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
4.设置程序计数器(PC) 。execve 做的最后一件事情就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。
在这里插入图片描述
图 7 - 7

7.8 缺页故障与缺页中断处理
在虚拟内存的习惯说法中, DRAM 缓存不命中称为缺页(page fault) 。图7-7 展示了在缺页之前我们的示例页表的状态。CPU 引用了VP 3 中的一个字, VP 3 并未缓存在DRAM 中。地址翻译硬件从内存中读取PTE 3, 从有效位推断出VP 3 未被缓存,并且触发一个缺页异常。缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,在此例中就是存放在PP3中的VP4 。如果VP4已经被修改了,那么内核就会将它复制回磁盘。无论哪种情况,内核都会修改VP4 的页表条目,反映出VP 4 不再缓存在主存中这一事实。(过程如图7-8 、 7-9 、 7-10 、7-11所演示。)
在这里插入图片描述
图 7 - 8
在这里插入图片描述
图 7 - 9
在这里插入图片描述
图 7 - 10
在这里插入图片描述
图 7 - 11

7.9动态存储分配管理
Printf会调用malloc,下面简述动态内存管理的基本方法与策略:
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器的类型:
 1.显式分配器: 要求应用显式地释放任何已分配的块。
 例如,C语言中的 malloc 和 free。
 2.隐式分配器: 应用检测到已分配块不再被程序所使用,就释放这个块
 比如Java,ML和Lisp等高级语言中的垃圾收集 (garbage collection)
记录空闲块的方法:
在这里插入图片描述
图 7 - 12

策略:
这里的策略指的就是显式的链表的方式分配还是隐式的标签引脚的方式分配还是分离适配。
带边界标签的隐式空闲链表分配器允许在常数时间内进行对前面块的合并。这种思想是在每个块的结尾处添加一个脚部,其中脚部就是头部的一个副本。如果每个块包括这样一个脚部,那么分配器就可以通过检查它的脚部,判断前面一个块的起始位置和状态,这个脚部总是在距当前块开始位置一个字的距离。
在这里插入图片描述
图 7 - 13
在这里插入图片描述
图 7 - 14
显式空间链表就是将空闲块组织为某种形式的显式数据结构。因为根据定义,程序不需要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体里面。例如,堆可以组织成一个双向空闲链表,在每个空闲块中,都包含一个前驱和后继指针,使首次适配的分配时间从块总数的线性时间减少到了空闲块数量的线性时间。为了分配一个块,必须确定请求的大小类,并且对适当的空闲链表做首次适配,查找一个合适的块。如果找到了一个,那么就(可选地)分割它,并将剩余的部分插入到适当的空闲链表中。如果找不到合适的块,那么就搜索下一个更大的大小类的空闲链表。如此重复,直到找到一个合适的块。如果空闲链表中没有合适的块,那么就向操作系统请求额外的堆内存,从这个新的堆内存中分配出一个块,将剩余部分放置在适当的大小类中。要释放一个块,我们执行合并,并将结果放置到相应的空闲链表中。
维护链表的顺序有:后进先出(LIFO),将新释放的块放置在链表的开始处,使用LIFO的顺序和首次适配的放置策略,分配器会最先检查最近使用过的块,在这种情况下,释放一个块可以在线性的时间内完成,如果使用了边界标记,那么合并也可以在常数时间内完成。按照地址顺序来维护链表,其中链表中的每个块的地址都小于它的后继的地址,在这种情况下,释放一个块需要线性时间的搜索来定位合适的前驱。平衡点在于,按照地址排序首次适配比LIFO排序的首次适配有着更高的内存利用率,接近最佳适配的利用率。

在这里插入图片描述
图 7 - 15

7.10本章小结
本章中,我们具体描述了hello的存储器地址空间,分为逻辑地址,物理地址,线性地址和虚拟地址四类。并且,了解了Intel逻辑地址到线性地址的变化——段式管理(课本上未提到过,特地上网查询)、线性地址到物理地址的变化——页式管理。还分别描述了TLB与四级页表支持下的VA到PA的变换、三级Cache支持下的物理内存访问、hello进程时fork时的内存映射、缺页故障与缺页中断处理,动态存储分配管理。

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
所有的I/ O 设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux 内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行,这就是Unix I/O接口。
8.2 简述Unix IO接口及其函数
Unix I/O接口:
1.打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访间一个I/O 设备。内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息。应用程序只需记住这个描述符。
2.Linux shell 创建的每个进程开始时都有三个打开的文件:标准输入(描述符为0) 、标准输出(描述符为1) 和标准错误(描述符为2) 。头文件< unistd.h> 定义了常量STDIN_FILENO 、STOOUT_FILENO 和STDERR_FILENO, 它们可用来代替显式的描述符值。
3.改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k, 初始为0。这个文件位置是从文件开头起始的字节偏移量。应用程序能够通过执行seek 操作,显式地设置文件的当前位置为K 。
4.读写文件。一个读操作就是从文件复制n>0 个字节到内存,从当前文件位置k 开始,然后将k增加到k+n 。给定一个大小为m 字节的文件,当k~m 时执行读操作会触发一个称为end-of-file(EOF) 的条件,应用程序能检测到这个条件。在文件结尾处并没有明确的“EOF 符号” 。类似地,写操作就是从内存复制n>0 个字节到一个文件,从当前文件位置k开始,然后更新k 。
5.关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放它们的内存资源。

Unix I/O函数:
1.进程是通过调用open 函数来打开一个已存在的文件或者创建一个新文件的:
int open(char *filename, int flags, mode_t mode);
open 函数将filename 转换为一个文件描述符,并且返回描述符数字。返回的描述符总是在进程中当前没有打开的最小描述符。flags 参数指明了进程打算如何访问这个文件,mode 参数指定了新文件的访问权限位。
返回:若成功则为新文件描述符,若出错为-1。
2.进程通过调用close 函数关闭一个打开的文件。
int close(int fd);
返回:若成功则为0, 若出错则为-1。
3.应用程序是通过分别调用read 和write 函数来执行输入和输出的。
ssize_t read(int fd, void *buf, size_t n);
read 函数从描述符为fd 的当前文件位置复制最多n 个字节到内存位置buf 。返回值-1表示一个错误,而返回值0 表示EOF。否则,返回值表示的是实际传送的字节数量。
返回:若成功则为读的字节数,若EOF 则为0, 若出错为-1。
ssize_t write(int fd, const void *buf, size_t n);
write 函数从内存位置buf 复制至多n 个字节到描述符fd 的当前文件位置。图10-3 展示了一个程序使用read 和write 调用一次一个字节地从标准输入复制到标准输出。
返回:若成功则为写的字节数,若出错则为-1。

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; 
}

其中“…”是可变形参的一种写法。 当传递参数的个数不确定时,就可以用这种方式来表示。其中的: (char*)(&fmt) + 4) 表示的是…中的第一个参数。依次+4便可得到prinf函数的其它参数。
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_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
最后的int INT_VECTOR_SYS_CALL表示要通过系统来调用sys_call这个函数。
sys_call的实现:
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,是为了保存中断前进程的状态。sys_call功能:显示格式化了的字符串。

从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码转换成ASCII码,保存到系统的键盘缓冲区之中。
getchar等调用read系统函数,通过系统调用读取按键ASCII码,直到接受到回车键才返回。
8.5本章小结
所有的I/ O 设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux 内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行,这就是Unix I/O接口。
同时,本章中我们就hello里面的函数对应unix的I/O细致地分析了一下I/O对接口以及操作方法,prinf与getchar函数是Unix I/O的封装,而真正调用的是write和read这样的系统调用函数,而它们又都是由内核完成的,之所以键盘能输入是因为引发了异步异常,在屏幕上会有显示是因为字符串被复制到了屏幕赖以显示的显存当中。

结论
在linux中,hello.c经过cpp的预处理、ccl的编译、as的汇编、ld的链接最终成为可执行目标程序hello,在shell中键入启动命令后,shell为其fork,产生子进程,产生子进程后shell为hello execve,映射虚拟内存,进入程序入口后程序开始载入物理内存,然后进入 main函数执行目标代码,CPU为运行的hello分配时间片执行逻辑控制流。当程序运行结束后,shell父进程负责回收hello进程,内核删除相关数据结构,一路上,hello经历了P2P,020的一生。
在初学编程语言时,一个简简单单,几秒后就被人抛弃的hello程序,没想到其蕴含的过程竟然如此得复杂又如此得精妙!在这其中,由前人奇思妙想,后人不断在这之上添砖加瓦的计算机系统大厦,扮演了最最最重要的角色!!!!在我看来,计算机系统设计与实现中,最美妙、最核心的理念就是“等效”一词!!!!最底层硬件CPU的设计人员,在上层的显示等效与更高效率的要求下,巧妙地构造出了流水线处理的模式;存储系统的设计人员,在实现读写等效的效果下,为了更好更省地让存储系统跟上CPU的速度,构想出了多级缓存结构;操作系统的设计人员,为了构建复杂的机器操作与人类的思维逻辑操作间的桥梁,设计出了诸如linux、windows等操作系统…每一层的实现者,为了上一层人员实现的便利,付出了无数的智慧与汗水,正是因为他们,才有了精密而美妙的计算机系统,才能让我们这些初学者像傻瓜一样地轻易实现一个实则复杂无比的操作!这是多么得伟大啊!!!!!!
计算机系统这门课程,锻炼了我的自学能力、理解能力和信息检索能力:遇到不会、不理解的,大胆上网寻找相应资料并学习。而贯穿整本书的等效思想、复杂问题分层实现以简单化的思想,更是丰富拓宽了我的视野,成为了我生活中解决问题的有力武器!

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

hello.c hello的源代码
hello.i hello.c经过预处理后的代码
hello.s hello.i经编译后的代码
hello.o hello.i汇编后得到的可重定位目标文件
hello hello.o链接后得到的可执行目标文件
hello.o.txt hello.o的反汇编文件
hello.txt hello的反汇编文件

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值