【无标题】

计算机系统

大作业

题     目  程序人生-Hello’s P2P 

专       业  工科试验班(医学类1               

学     号  2022113284            

班     级  2252002               

学       生  李信哲               

指 导 教 师  史先俊                 

计算机科学与技术学院

2024年5月

摘  要

当我们初次踏入计算机系统的学习之旅时,经历了一次深入的探索,仿佛在计算机世界的广袤天地间遨游。在这个旅程中,我们发现,一个看似简单的源代码文件(比如hello.c),为了能在计算机上执行,必须经过一系列精心的转变:预处理、编译、汇编直至链接,最终化身为二进制的可执行文件,这是计算机能够直接理解和执行的语言。

但计算机的世界并非孤立于这个二进制文件,它是一个由处理器、输入/输出设备、主存储器等硬件元素精密构建的复杂生态系统。在这个系统内,hello程序的运行仅仅是冰山一角,计算机同时驾驭着多个程序的并行运作,这背后隐藏的关键概念便是“进程”。

回想在C语言编程的课堂上,我们亲手编写并运行了hello.c,见证了它的诞生与执行。而现在,我们的任务是以hello.c为透镜,从一个更加宏观和深入的视角——计算机系统的层面,去剖析和追踪它如何从一段静态的程序代码(program),一步步跃动成活跃的进程实体(process),揭秘其背后的机制与历程。

关键词:计算机系统;预处理;编译;汇编;链接;进程;存储;I/O……;                           

目  录

第1章 概述............................................................................................................. - 4 -

1.1 Hello简介...................................................................................................... - 4 -

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

1.3 中间结果......................................................................................................... - 4 -

1.4 本章小结......................................................................................................... - 4 -

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

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

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

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

2.4 本章小结......................................................................................................... - 5 -

第3章 编译............................................................................................................. - 6 -

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

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

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

3.4 本章小结......................................................................................................... - 6 -

第4章 汇编............................................................................................................. - 7 -

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

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

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

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

4.5 本章小结......................................................................................................... - 7 -

第5章 链接............................................................................................................. - 8 -

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

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

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

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

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

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

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

5.8 本章小结......................................................................................................... - 9 -

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

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

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

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

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

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

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

6.7本章小结....................................................................................................... - 10 -

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

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

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

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

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

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

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

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

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

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

7.10本章小结..................................................................................................... - 12 -

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

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

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

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

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

8.5本章小结....................................................................................................... - 13 -

结论......................................................................................................................... - 14 -

附件......................................................................................................................... - 15 -

参考文献................................................................................................................. - 16 -

第1章 概述

1.1 Hello简介

P2P:当我们完成hello.c这个C语言程序的编写后,它以纯文本的形式保存在磁盘上。执行程序的步骤随即展开,由编译器引导整个过程。首先,编译器读取hello.c,通过预处理生成一个中间C语言文件hello.i。接着,此文件被进一步编译成汇编语言版本hello.s。汇编器接手后,将汇编代码转化为机器语言指令,并组织成可重定位的目标程序,存储在hello.o这个二进制文件中。最后,链接器将所有相关目标文件整合,形成最终的可执行文件hello,至此,计算机能够直接执行它了。

在操作系统的管理下,当我们在Bash命令行中启动hello,系统通过fork机制为它创建了一个全新的子进程,标志着hello作为独立进程在系统中占有一席之地。这一过程确保了每个程序都能在自己的专属环境中运行,互不干扰。

020:至于程序在内存中的生命轨迹,实际上是从无到有,再回归于无的过程。最初,程序并未驻留于内存,内存状态为空(0)。操作系统在执行execve指令来启动hello时,为它分配了一片虚拟内存区域,并将程序映射到实际的物理内存上。随着程序执行完毕,操作系统负责回收相关的程序资源及所占用的内存空间,内存再次回到初始的空闲状态(0),完成了一次循环。这个过程不仅展示了计算机资源的有效管理和利用,也反映了程序执行的完整生命周期。

1.2 环境与工具

1.2.1 硬件环境

X64 CPU; 2GHz; 16G RAM; 512GHD Disk

1.2.2 软件环境

Windows10 64位; Vmware 14; Ubuntu20.04

1.2.3 开发工具

Visual Studio Code; vi/vim/gpedit+gcc

                       

1.3 中间结果                      

①原始代码hello.c。

②预处理后的代码hello.i。

③编译后的汇编语言代码hello.s。

④可重定位目标文件hello.o。

⑤hello.o的objdump结果helloo.txt。

⑥可执行文件hello。

⑦hello的objdump结果hello.txt。

1.4 本章小结

本章主要介绍了hello.c程序P2P,020的过程。列出了本次实验所需的环境和工具以及过程中所生成的中间结果。

第2章 预处理

2.1 预处理的概念与作用

预处理是编程语言编译过程中的一个早期阶段,它发生在源代码被编译器转换为机器代码之前。预处理器是一个独立的程序,通常作为编译器套件的一部分,负责读取源代码文件,并按照预处理指令(以特定符号标识的指令,如#include、#define、#ifdef等)对源代码进行修改或替换,从而生成一个新的源代码文件,供后续编译阶段使用。预处理主要具有以下几个核心概念和作用:

预处理的作用主要包括:

提高代码可维护性:通过文件包含和宏定义减少重复代码,便于修改和更新。

模块化编程:支持代码的复用和组织,增强程序结构清晰度。

配置灵活性:条件编译使程序能够在不同的配置或平台上编译出适应各自需求的版本。

简化编程任务:通过宏自动完成一些繁琐或复杂的文本替换工作。

2.2在Ubuntu下预处理的命令

在终端内输入命令gcc -E hello.c,在屏幕上得到hello.c的预处理结果(如图)。为方便起见我们重定向gcc的输出,将结果保存到hello.i文件内。

2.3 Hello的预处理结果解析

查看hello.i文件,在最开头,是hello.c涉及到的所有头文件的信息

然后,先是这些头文件用typedef定义的诸多类型别名

然后的内容是被include的头文件们的主体内容,包括大量的函数声明和少部分struct定义等。它们都完全经过预处理器的宏展开。

在文件的最后,才是真正的hello.c的内容

2.4 本章小结

本节阐述了预处理的概念和具体的作用。并亲手在Ubuntu执行了预处理命令,生成了hello.i文件,通过大致浏览hello.i文件,更好地理解了预处理的概念和作用。

(第2章0.5分)

第3章 编译

3.1 编译的概念与作用

1 编译的概念:

编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。

2 编译的作用:

在编译阶段中,gcc首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,gcc把代码翻译成汇编语言。在编译阶段,编译器还能起到优化的作用,优化处理是编译系统中一项比较艰深的技术。它涉及到的问题不仅同编译技术本身有关,而且同机器的硬件环境也有很大的关系。

       

3.2 在Ubuntu下编译的命令

在终端里输入命令gcc -S hello.i -o hello.s -fno-PIC -no-pie -m64,得到编译结hello.s。如下图所示:

3.3 Hello的编译结果解析

汇编文件头部声明:

.file 源文件(指从hello.i汇编得来)

.text代码节

.rodata 制度代码段

.align 代码对齐方式

.global 全局变量

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

.string 声明了两个字符串分别为.LC0; .LC1

3.1.1 数据:

1. 无宏变量等,只有一个全局(global)为main函数:

2. 字符串:hello程序在调用printf的时候使用了两个常量字符串,它们存在hello程序的只读数据节,在hello.s中是这样的:

3. 局部变量:将栈指针-4,为局部变量int i开辟一个四字节的空间;

4. 数组:该c程序只有main函数接收的argv[ ]数组;

3.1.2 赋值操作:将寄存器中的数据或者立即数,加载(mov)到寄存器(或指针)中,其中movl,movq分别表示它们操作的数据大小不同:l表示双字,数据占4个字节;q表示四字,数据占8个字节。

3.1.3 算术操作:将寄存器存储的数据加上立即数24之后再存入到寄存器;

3.1.4 关系操作:cmp比较指令,指令执行的结果是返回条件码;

3.1.5 控制跳转:通常在关系操作指令的下一条指令即为跳转指令,跳转指令根据cmp指令返回的条件码进行跳转,实现控制跳转;

3.1.6 函数操作:通过call指令调用过程;如图,分别调用了头文件提供的printf, sleep, getchar,exit,puts函数。

3.4 本章小结

在高级语言编程环境下,程序员无需关注底层硬件操作的具体实现细节,这些细节被编程语言及其编译环境有效隐藏。尽管这种抽象提高了编程效率和可读性,但这也意味着直接在计算机硬件上执行代码是不可能的,因为硬件仅能识别并执行低级的机器语言指令。为了解决这一层级间的隔阂,编译器扮演了核心角色,它充当了高级语言与机器语言之间的桥梁。

具体而言,通过实例分析如将hello.i转换为hello.s的过程,我们可以深刻洞察编译器的工作机制及其重要性。这个过程不仅揭示了编译器如何将人类可读的高级指令转换为计算机可执行的低级指令,还让我们直观地认识到汇编语言作为中间语言,在整个程序生命周期中的衔接功能。它紧密联系着高层逻辑设计与底层硬件执行,是实现语言高级抽象与机器指令集之间语义映射的关键。

进一步地,通过在“计算机系统”课程的实践学习中积累的经验,我们得以强化这种理解,学会高效地双向穿梭于C语言代码与其对应的汇编表示之间。这种能力不仅是对编译原理的深入掌握,也是对计算机系统工作原理的深刻认识,使我们能够更加灵活地优化代码性能,或者在必要时进行底层调试。因此,理解编译器如何将高级语言转换为汇编代码,不仅增进了我们对编程语言抽象层次的理解,也强化了我们对计算机系统底层执行机制的掌握。

(第32分)

第4章 汇编

4.1 汇编的概念与作用

概念上,汇编语言是面向计算机硬件的编程层次,它为程序员提供了一种方式,能够直接控制和访问计算机硬件的特性,比如寄存器、内存地址以及处理器指令等。每个汇编语言指令通常对应一条特定的机器语言指令,编译(汇编)过程就是将这些助记符转换成计算机可以直接执行的机器码。

作用包括但不限于:

1. 底层控制:汇编语言允许程序员精确控制硬件资源,这对于需要高度优化的性能敏感型应用(如操作系统内核、设备驱动、嵌入式系统)至关重要,因为可以手动调整代码以充分利用硬件能力,减少资源消耗或提高运行速度。

2. 系统编程:在开发操作系统、 bootloader等系统软件时,由于需要与硬件直接交互,汇编语言成为了不可或缺的工具。

4.2 在Ubuntu下汇编的命令

    在终端中对hello.s使用命令gcc -c,如图所示:

4.3 可重定位目标elf格式

使用readelf解析汇编器生成的可重定位目标文件hello.o,结果如下:

ELF Header:

从ELF Header我们可以看到hello.oELF格式的一些基本信息。比如main函数的指令编码、操作系统的版本,节头文件的起始地址等等。

Section Header Table:

在Section Header Table中,我们可以看到各Section的描述信息,其中.text和.data是我们在汇编程序中声明的Section,而其它Section是汇编器自动添加的。

rela.text:

rela.text包含需要重定位的信息,当链接器链接.o文件时,会根据重定位节的信息计算正确的地址,重定位.rela.text中的信息。

从下图中我们可以看到.rela.text中的不同类型的信息。

symtab:

symtab 是一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。   例如我们在hello.c中分析看到的,exit, printf, sleep, getchar函数,以及我们在hello.s中看到的调用的puts, atoi过程。

4.4 Hello.o的结果解析

  在终端中输入命令objdump -d -r hello.o,得到hello.o中可执行代码的反汇编结果:

 

对比`hello.s`与反汇编后的输出,一个显著的差异在于后者在每一行汇编指令前附加了相应的十六进制机器码。这揭示了从汇编语言到机器语言的转换本质:`hello.s`作为汇编程序的代表,采用了人类可读的符号形式,虽然相比高级语言更为底层,但仍保持了一定程度的抽象性,便于程序员理解与编写;反之,通过反汇编得到的输出,则直接展示了计算机硬件能够直接识别和执行的二进制指令集。

  1. 分支转移:hello.s的分支转移是通过指令jmp或je, jne, ... ,等直接跳转到某一段代码,例如:

分别跳转到.L2和.L4代码段。

而在反汇编文件中,并不存在汇编语言中的代码段地址,而是直接跳转到当前过程的起始地址加上偏移量得到的直接目标代码地址,如下图所示:

     2. 函数的调用:在汇编文件中,call指令直接调用函数,call后紧跟函数的名字,如下图:

        ​​​​​​​

在深入分析反汇编代码时,我们注意到一个有趣的特性:`call`指令看似指向其紧接着的下一条指令。这一现象根源于程序构建过程中的一个重要阶段。当源代码经过汇编转换为对象文件(如`hello.o`)时,生成的是不完整的机器语言版本,特别是对于那些调用外部库函数的部分。此时的对象文件缺乏对C库函数等外部引用的实际地址绑定,因为这些地址在单独编译每个源文件时是未知的。

4.5 本章小结

在先前的讨论环节中,我们成功地将高级语言的初步翻译——`hello.s`汇编代码,推进到了更接近计算机硬件理解层面的一步,即通过汇编过程转换为`hello.o`可重定位目标文件。这个过程标志着源代码向机器可直接解读的语言形式的过渡。尽管汇编语言本身已是对机器指令的一种抽象和人性化表述,但它仍然是计算机无法直接执行的中间状态;因此,通过汇编器的转化,这些指令被映射到具体的机器码,每一个汇编指令对应着一组精确的二进制序列。

(第41分)

第5章 链接

5.1 链接的概念与作用

链接是程序构建过程的一个核心步骤,它发生在编译器将源代码转换为目标代码之后。主要目的是将一个或多个目标文件(.o文件)以及所需的库文件合并成一个完整的、可执行的程序或库文件。

5.2 在Ubuntu下链接的命令

ld -o hello -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o hello.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o -no-pie

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

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

ELF Header:

Section Header Table:

5.4 hello的虚拟地址空间

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

使用edb可以查看hello虚拟地址空间。从ELF开始,我们可以知道起始地址为0x400010;

5.5 链接的重定位过程分析

1.hello反汇编文件中,每行指令都有唯一的虚拟地址,而hello.o的反汇编没有。这是因为hello.o经过链接,已经完成重定位,每条指令的地址关系已经确定;

2.在hello反汇编文件中,出现了很多hello.o中没有的过程。这同样是经过重定位,然后链接的结果,链接器将需要重定位的call调用的过程的.o连接到hello当中,并确定下地址关系。所以在hello反汇编文件中,我们可以看到hello.s中曾分析过所调用的函数:

在链接器完成符号解析之后,就把代码中的每个符号引用和正好一个符号定义关联起来。此时,链接器就知道它的输入目标模块中的代码节和数据节的确切大小,然后便可以进行重定位。

链接器将所有相同类型的节合并为同一个类型的新的聚合节,然后,链接器将运行时内存地址赋给新的聚合节,赋给输出模块定义的每个节,体积赋给输出模块定义的每个符号。当这一步完成时,程序中的每条指令和全局变量都有唯一的运行时内存地址了。链接器修改代码节和数据节中对每个符号的引用,使得他们指向正确的地址,要执行这一步,链接器依赖于可冲定位目标模块中称为重定位条目的数据结构。

5.6 hello的执行流程

1.使用edb执行hello,首先,最初的程序地址会在0x7631d8403290处,这里是hello使用的动态链接库ld-2.31.so的入口点_dl_start:

2.然后,程序跳转到_dl_init,在经过了一系列初始化后,跳到hello的程序入口点_start;

3.然后程序通过一个间接call指令跳到动态链接库ld-2.31.so的__libc_start_main处,这个函数会进行一些初始化,并负责调用main函数;

4. 然后它会调用动态链接库中的__cxa_atexit函数,它会设置在程序结束时需要调用的函数表;

5. 然后返回到__libc_start_main继续,然后调用hello可执行文件中的__libc_csu_init函数,这函数是由静态库引入的,也是做一些初始化的工作;

6. 然后返回到__libc_start_main继续,然后调用动态链接库里的_setjmp函数,应该是设置一些非本地跳转;

7.然后返回到__libc_start_main继续,我们就要开始调用main函数了;

8. 现在我们其实不再关心main本身的程序,只是想看看main结束后的事情。由于我们在edb运行hello的时候并未给出额外的命令行参数,因此它会在第一个if处通过exit(1)直接结束程序;

9. 通过hello本身携带的exit函数,程序会跳转;

10. 之后,在进行了若干操作后,程序退出。

0000000000401000 <_init>

0000000000401020 <.plt>

0000000000401090 <puts@plt>

00000000004010a0 <printf@plt>

00000000004010b0 <getchar@plt>

00000000004010c0 <atoi@plt>

00000000004010d0 <exit@plt>

00000000004010e0 <sleep@plt>

00000000004010f0 <_start>

0000000000401120 <_dl_relocate_static_pie>

0000000000401130 <deregister_tm_clones>

0000000000401160 <register_tm_clones>

00000000004011a0 <__do_global_dtors_aux>

00000000004011d0 <frame_dummy>

00000000004011d6 <main>

0000000000401270 <__libc_csu_init>

00000000004012e0 <__libc_csu_fini>:

00000000004012e8 <_fini>

5.7 Hello的动态链接分析

Hello程序在加载可执行文件时,自动加载了动态链接库ld-2.31.so。hello程序的一些地方可能会引用这个动态链接库里的符号(比如函数调用),这一机制是通过PLT表和GOT表实现的,它们的每一个条目都对应动态链接库中的符号引用。在readelf中我们可以在节表里看到关于PLT和GOT的信息(它们对应两个节):

在程序一开始,先执行_dl_start和_dl_init,_dl_init能够修改PLT和GOT,这一过程相当于“注册”动态链接库的符号,使得hello在后面的正常运行中能够引用它们(实现诸如间接跳转等行为)。

我们使用edb来验证这一过程,监测0x404000地址处PLT的数据变化。这是调用_dl_init之前的:

这是调用_dl_init后的,可以发现PLT表发生了变化:

因此这样我们就通过实践领会了动态链接的机制。

5.8 本章小结

通过上述实践,我们深入剖析了`hello.o`目标文件、静态库与动态链接库这三要素如何通过精密的链接机制相互融合,构筑了程序的骨架。这一过程不仅揭示了从程序加载至执行终止的完整旅程,还凸显了静态库与动态链接库的隐性贡献——它们虽在幕后,却是支撑程序顺利运作不可或缺的支柱。此番探索深刻表明,一个简单的“hello”程序背后,实则蕴含着远超表面的复杂性与精妙设计。

(第51分)

第6章 hello进程管理

6.1 进程的概念与作用

进程是现代操作系统中的一个核心概念,它是程序执行的一个实例,是系统进行资源分配和调度的基本单位。具体来说,当一个程序被加载到内存并开始执行时,就成为了一个进程。每个进程都拥有独立的地址空间,这意味着它们可以拥有自己的内存、数据、代码以及打开的文件等资源。此外,进程还拥有状态(如就绪、运行、阻塞等),优先级以及唯一的进程标识符(PID)。

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

1.读取命令行的字符串

2.分割字符串,读取对应的命令

3.判断是否为内置命令,如果是则直接运行,不是则fork一个子进程运行

4.子进程用execve运行对应的命令

6.等待前台进程结束

7.返回读去阶段

6.3 Hello的fork进程创建过程

    当Shell接收到用户输入的命令`./hello`时,它随即启动一个解析流程,识别出这是一个要求加载并执行指定本地可执行文件的指令。紧接着,Shell通过一系列精心编排的操作来响应这一请求:首先,它构造了一个与该命令相关的作业实体,这是对即将执行任务的一种抽象表示。随后,采用系统调用`fork()`来派生一个子进程,这个新产生的子进程近乎完美地复制了父进程(即Shell自身)的全貌——共享相同的代码段、数据段、堆内存、关联的共享库以及栈结构。尽管如此,它们在身份标识上保持着清晰的区别,主要体现在各自拥有独一无二的进程ID(PID)及`fork()`调用在父子进程间返回的不同值,这为系统提供了区分和管理这些进程的机制。

进一步地,为了增强作业的组织与管控,Shell紧接着利用`setpgid()`系统调用,将这个新生的子进程纳入一个专为其设立的新进程组中。这一举措不仅界定了`./hello`命令执行所涉及的进程集合边界,还为Shell提供了一种高效手段:通过向该进程组统一发送信号,即可实现对整个作业生命周期的灵活管理与协调。这种机制确保了Shell能够对用户发起的命令执行进行有效的监督与响应,体现了操作系统在进程和作业管理层面的严谨设计与高度灵活性。

6.4 Hello的execve过程

在新创建的子进程环境中,紧随其后的关键步骤是由该子进程主动调用`execve()`系统调用,这标志着一个深刻的转变:它不仅仅是加载`hello`可执行文件到当前进程空间那么简单,而是实施了一场彻底的变革。此过程涉及将子进程原先的用户空间内存布局彻底摒弃,取而代之的是基于虚拟内存管理机制,精心映射`hello`程序的各组成部分——包括代码段、数据段等——到相应的地址空间之中。这是一次内存空间的重构,确保了`hello`程序所需的一切资源得以精准定位,从而构建起一个新的用户区域,专为执行`hello`程序而准备。

进一步地,`execve()`不仅仅局限于主体程序的加载,它还负责加载`hello`程序所依赖的共享库,如`ld-2.31.so`,通过虚拟内存映射技术,这些共享库同样被精细地嵌入到进程的地址空间中,确保了程序执行所需的全部组件齐备无缺。这一系列操作展示了现代操作系统如何高效地复用和共享系统资源,减少内存冗余,提升执行效率。

最终阶段,随着所有必要组件的成功部署,子进程的控制流优雅地转移到了`hello`程序的入口点,标志着执行阶段的正式启动。这一跃迁不仅是控制权的传递,更是从准备阶段到实际执行的华丽转身,确保了程序逻辑的连贯执行,完美体现了操作系统在程序加载与执行管理上的精密设计与高效执行策略。

6.5 Hello的进程执行

进程的正常执行仰赖于一个完整且稳定的运行环境,即其上下文,这涵盖了程序代码、数据结构、栈信息、寄存器内容及所占用资源的集合,任何时刻,这些构成元素的完整性都是保证程序流畅运行的前提条件。尽管如此,为了实现多任务处理和资源的有效分配,操作系统必须频繁地进行进程切换,其间需确保上下文的妥善保管与恢复,以维持各进程运行的连贯性和独立性。

当“hello”进程正活跃运行时,突如其来的时钟中断作为外部事件介入,触发了从用户态到内核态的权限跃迁。这一转换中,操作系统内核迅速介入,承担起保护“hello”进程现场的重任,将其运行时的所有关键状态保存下来。随后,借助精细的调度算法,内核选定下一个待执行的进程“B”,加载其先前保存的上下文信息,巧妙地实现控制权的平滑过渡,与此同时,处理器状态亦回归用户态,继续执行新进程的任务。

操作系统通过为每个进程预设时间片,精确调控了各进程占用CPU的时段,这一机制确保了系统资源的均衡分配与利用效率的最大化。以“hello”进程为例,当它主动调用“sleep”函数进入休眠状态时,实际上是通过系统调用这一机制向内核发出请求,内核响应后保存“hello”的上下文,将其标记为休眠,并再次进行进程调度。直至预定的休眠周期结束,另一个时钟中断的契机下,内核检测到“hello”已满足唤醒条件,便适时恢复其先前保存的上下文环境,重新激活该进程,处理器随之回到用户态,继续执行“hello”的后续指令。

这一系列复杂而有序的操作,展示了操作系统如何通过上下文切换与时间片分配机制,实现了进程间的高效协作与资源管理,保障了系统整体的稳定运行与多任务处理能力,深刻体现了现代操作系统设计的精妙与高效。

6.6 hello的异常与信号处理

1.hello执行过程中可能会出现的异常:

中断:在hello程序执行的过程中可能会出现来自处理器外部的I/O设备的信号引起的异常;

陷阱:陷阱是有意的异常,是执行一条指令的结果。陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的系统调用。所以,从shell中输入./hello开始,fork,  execve,  sleep,  exit函数都会引起系统调用,从而触发陷阱;

故障:故障是有错误情况引起,可能能被故障处理程序修正。在执行hello程序的时候,可能会发生缺页异常。

终止:终止是不可恢复的错误,通常是一些硬件的错误。在hello执行过程可能会出现DRAM或者SRAM位损坏的奇偶错误。

2.异常会发送的信号:

信号名称        默认行为              相应事件

SIGINT

Terminate

来自键盘的中断(通常是Ctrl+c)

SIGKILL

Terminate

强迫进程终止(kill -9)

SIGTERM

Terminate

进程终止(不带参数时kill默认发送的信号)

SIGSTOP

Stop

停止进程执行

SIGCONT

Continue

如果进程已停止则恢复执行

SIGSEGV

Dump

无效的内存引用

SIGALRM

Terminate

实时定时器时钟

SIGTSTP

Stop

从tty发出停止进程(Ctrl+z)

3.各命令以及运行结果:

正常运行结果:

不停乱按(除了后续的特定键入组合):不影响当前进程执行;

Ctrl-Z;(发出SIGTSTP信号)

结果,进程直接停止;

Ctrl-C;(发出SIGINT信号)

结果,进程中断;

Ctrl-z后运行ps;

Ctrl-z后运行jobs;

Ctrl-z后运行pstree;

Ctrl-z后运行fg;

                         

                       

6.7本章小结

经过细致的分析与实践探索,我们深刻洞察到,尽管"hello"程序在表面上展现出一种简明的运行表象,其背后却蕴藏着一系列错综复杂且高度协同的操作机制。实际上,"hello"程序的执行远非孤立地占用CPU与内存资源那般单纯,而是置身于操作系统精密的进程调度框架之下,与其他进程并行交错地展开工作,共享系统资源的同时,响应着系统动态调配的需求。

在这场微观层面的执行盛宴中,"hello"进程的旅程并不总是一帆风顺,它需面对种种意料之外的挑战——异常与信号的不期而至。这些异步事件,作为操作系统控制流转向与信息交流的桥梁,扮演着至关重要的角色。它们不仅能够中断现行任务,促使控制权在不同代码路径间跳跃,还能够作为一种机制,实现进程间乃至系统与进程的低延迟通信,确保了系统的灵活性与健壮性。

因此,"hello"程序的执行过程,实质上是对操作系统底层机制的一次深度演绎:从进程调度的并发执行,到异常与信号处理的异步响应,每一环节都彰显了现代操作系统设计的精妙与复杂性。这一过程不仅是对单一程序行为的剖析,更是对操作系统如何在有限资源下,通过高度动态与协同的管理,实现高效、可靠运行原理的深刻理解。

(第61分)

第7章 hello的存储管理

7.1 hello的存储器地址空间

1. 逻辑地址:是指由程序产生的与段相关的偏移地址部分。页式存储器的逻辑地址由两部分组成:页号和页内地址。[段标识符 : 段内偏移地址] 的表示形式,其中的段内偏移地址就是指逻辑地址;

2. 线性地址:是逻辑地址到物理地址变换之间的中间层。在分段部件中逻辑地址是段中的偏移地址,然后加上基地址[段基地址+段内偏移地址]就是线性地址;我们在hello的反汇编代码中见过这种表示方法;

3. 虚拟地址:也就是线性地址。hello的反汇编文件中的0x401000就是一种虚拟地址;

4. 物理地址:CPU地址总线传来的地址。物理地址中很大一部分是留给内存条中的内存的。在没有使用虚拟存储器的机器上,虚拟地址被直接送到内存总线上,使具有相同地址的物理存储器被读写;而在使用了虚拟存储器的情况下,虚拟地址不是被直接送到内存地址总线上,而是送到存储器管理单元MMU,把虚拟地址映射为物理地址。

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

1、逻辑地址=段选择符+偏移量;

2、每个段选择符大小为16位,段描述符为8字节(注意单位);

3、GDT为全局描述符表,LDT为局部描述符表;

4、段描述符存放在描述符表中,也就是GDT或LDT中;

5、段首地址存放在段描述符中;

每个段的首地址都存放在自己的段描述符中,而所有的段描述符都存放在一个描述符表中(描述符表分为全局描述符表GDT和局部描述符表LDT)。而要想找到某个段的描述符必须通过段选择符才能找到。

所以简单地说,从逻辑地址到线性地址,首先要获得段偏移有效地址;然后取出段寄存器对应的描述符的基地址;最后将二者计算相加,于是得到了线性地址。

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

虚拟地址(也就是线性地址)由虚拟页号VPN与虚拟页偏移量VPO组成;物理地址由物理页号PPN和物理页偏移量PPO组成。

虚拟内存被分割为成为虚拟页的大小固定的块来解决虚拟内存的存储问题。页式管理将虚拟地址与内存地址建立一一对应的页表。

PTBR指向当前页表。MMU利用VPN 来选择适当的PTE。例如VPN 0选择PTE 0。将页表条目中物理页号PPN与虚拟地址的VPO串联起来,就得到相应的物理地址。其中由于虚拟地址与物理地址的偏移量大小相同,所以PPO和VPO是相同的。

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

Core i7采用四级页表层次结构,以Core i7的地址翻译为例

36的VPN被划分成四个9位的片,每个片被用作到一个页表的偏移量。CR3寄存器包含L1页表的物理地址。VPN1提供到一个L1 PTE的偏移量,这个PTE包含L2页表的基地址。VPN2提供到一个L2 PTE的偏移量,以此类推。最后得到PPN,而PPO与VPO 仍相同。

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

由虚拟地址翻译得到物理地址之后,将物理地址分为缓存偏移CO、缓存组索引CI以及缓存标记CT。

首先,利用组索引CI来寻找我们的地址是否在Cache中有对应的组;然后利用标记CT来判断我们的内容是否在Cache中。若命中,则访问我们的物理地址;若不命中,则进行下一层Cache的索引、访问,以此类推。

7.6 hello进程fork时的内存映射

当 fork 函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的 PID。为了给这个新进程创建虚拟内存,它创建了当前进程的 mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有写时复制。

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

7.7 hello进程execve时的内存映射

execve 函数在当前进程中加载并运行包含在可执行目标文件 hello 中的程序,用 hello 程序有效地替代了当前程序。加载并运行 hello 需要一下几个步骤:

删除已存在的用户区域;

映射私有区域:为新程序 hello 的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的;

映射共享区域:如果 hello 程序与共享对象(或目标)链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内;

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

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

缺页故障概念:当指令引用一个虚拟地址,而与该地址相对于的物理页面不在内存中,因此必须从磁盘中取出时,就会发送缺页故障。

缺页中断处理:在试图翻译某个虚拟地址A时,触发了一个缺页异常。这个异常转移到内核的缺页异常处理程序。

缺页异常处理程序要进行判断:

如果虚拟地址是一个不存在的页面,则视为段错误;

如果虚拟地址不合法,比如违反了只读的约定,则触发保护异常机制;

如若虚拟地址可访问且合法,那么视为正常缺页。对于正常缺页,程序会选择一个牺牲页,牺牲掉它,然后将虚拟地址不知道内存中,并更新PTE。这样,再访问虚拟地址对应的物理地址,就不会缺页了。

7.9动态存储分配管理

    虽然可以使用低级的mmap和munmap函数来创建和删除虚拟内存区域,但是C程序员还是会觉得当运行时需要额外虚拟内存时,用动态内存分配器更方便,也有更好的可移植性。

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

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

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

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

基本方法与策略:

隐式空闲链表

任何实际的分配器都需要一些数据结构,允许它来区别块边界,以及区别已分配块和空闲块。大多数分配器将这些信息嵌入块本身,有一种方法是:一个块除了是由一个字的头部、有效载荷、可能的一些额外的填充组成外,还有一个与头部相同的脚部组成。头部和脚部编码了这个块的大小(包括头部和所有的填充),以及这个块是已分配的还是空闲的。如果我们强加一个双字的对齐约束条件,那么块大小就总是8的倍数,且块大小的最低3位总是0。因此,我们只需要内存大小的29个高位,释放剩余的3位来编码其他信息。在这种情况中,我们用其中的最低位(已分配位)来指明这个块是已分配的还是空闲的。

头部后面就是应用调用malloc时请求的有效载荷。有效载荷后面是一片不使用的填充块,其大小可以是任意的。需要填充有很多原因。比如,填充可能是分配器策略的一部分,用来对付外部碎片。或者也需要用它来满足对齐要求。

我们称这种结构称为隐式空闲链表,是因为空闲块是通过头部中的大小字段隐含地连接着的。分配器可以通过遍历堆中所有的块,从而间接地遍历整个空闲块的集合。在带边界标签的隐式空闲链表中,我们的脚部就标记了一个块的结束。

合并的时候分配器就可以通过检查脚部来检查前一块的状态和大小了。

显式空闲链表

还有一种更好的方法,是将空闲块组织为某种形式的显示数据结构是一种更好的方法,因为根据定义,程序不需要一个空闲块的主体,所以实现空闲链表数据结构的指针可以存放在这些空闲块的主体里面。

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

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

另一种方法是按照地址顺序来维护链表,其中链表中的每一个块的地址都小于它后一个块的地址,在这种情况下释放一个块需要线性时间的搜索来定位合适的前驱。

                           

                       

7.10本章小结

本章主要介绍了hello程序的存储管理,辨析了逻辑地址、线性地址、虚拟地址和物理地址的关系;又分析了逐步地将地址翻译为最终物理地址的翻译;更深层次的理解了页表、Cache、内存映射的概念,对fork、execve有了新的理解视角;又介绍了动态内存管理的基本方法和策略。

(第7 2分)

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

     在Linux中,所有的IO设备(网络、磁盘、终端等)都被模型化为文件,所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备优雅地映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,使得所有的输入和输出都能以一种统一且一致的方式来执行。

8.2 简述Unix IO接口及其函数

Unix IO接口可以实现4种基本操作:

①打开文件,应用程序要求内核打开相应的文件,来宣告它想要访问一个IO设备,内核返回这个文件的描述符以标识这个文件。Shell创建的每个进程开始时都有3个打开的文件:标准输入(stdin)、标准输出(stdout)和标准错误(stderr)。

②改变当前的文件位置,应用程序通过执行seek操作,显式地设置文件的当前位置为k。

③读写文件,读操作就是从当前位置k开始,从文件复制n个字节到内存,然后将k增加到k+n,当k超出文件长度时应用程序能够通过EOF检测到。而写操作则是从内存复制n个字节到一个文件,从当前文件位置k开始,然后更新k。

④关闭文件,当应用完成了对文件的访问之后,它就通知内核关闭这个文件,内核释放文件打开时创建的数据结构和内存资源。

Unix I/O函数:

进程通过调用open函数来打开一个已存在的文件或者创建一个新文件的:

int open(char *filename, int flags, mode_t mode)

open函数将filename转换为一个文件描述符,并且返回描述符数字;flags参数也可以是一个或者更多位掩饰的或,为写提供给一些额外的指示;mode参数指定了新文件的访问权限位。

1.进程 通过调用close函数关闭一个打开的文件:

int close(int fd)

2.应用程序是通过分别调用read和write函数来执行输入和输出的:

ssize_t read(int fd, void *buf, size_t n);

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

ssize_t write(int fd, const void *buf, size_t n);

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

3.通过调用lseek函数,应用程序能都显示地修改当前文件的位置。

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;

}

在程序输出流程的深入解析中,我们可以这样阐述:初始化阶段,`printf`功能预设了一片缓冲区,旨在暂存即将外显的信息。紧接着,运用`vsprintf`函数,程序按既定格式精心构造字符串内容,细心嵌入这片缓冲区之中。此过程构建了信息输出的基石。

随后,接力棒传递给了`write`函数。此函数担当起信息传递使者的角色,它通过系统调用这一机制,巧妙跨越用户态与内核态的边界,将缓冲区内封装好的字符串递交至操作系统内核。在这里,内核的显示驱动程序接过重任,它不仅解析接收到的字符串,还依据指定的字体样式,将其转换为具体的像素矩阵,为视觉呈现铺设蓝图。这些像素数据被精准定位到显示VRAM(视频随机访问存储器)中的特定区域,为屏幕展示做足准备。

最终阶段,显示芯片依据固定的刷新速率,顺序扫描VRAM,逐一提取像素信息,并通过高度协调的信号传输通道,将每个像素点的RGB色彩成分投射至液晶显示屏的对应坐标上,实现了从代码逻辑到视觉印象的华丽转变。这一连串精密协作,不仅揭示了从软件指令到硬件显示的复杂路径,也体现了现代计算体系中软硬件深度融合的工作哲学。

8.4 getchar的实现分析

getchar的实现大致如下:

int getchar(void)

{

      static char buf[BUFSIZ];

      static char* bb=buf;

      static int n=0;

      if(n==0)

      {

             n=read(0,buf,BUFSIZ);

             bb=buf;

      }

      return(--n>=0)?(unsigned char)*bb++:EOF;

}

在理解输入处理的机制时,可以这样详细解析:进程初始化时,`getchar`功能关联了一个静态的输入缓冲区,该缓冲区扮演着中间人角色,用于暂存外部输入数据。当此缓冲区处于空闲状态,即无数据待处理时,`getchar`会委托`read`函数执行数据采集任务。这一步骤通过系统调用的桥梁,引领控制权从用户空间过渡至内核空间,期间,请求进程会被挂起,进入休眠状态,等待数据的到来。

此间,一个关键的转折点在于用户通过键盘的物理交互。一旦键盘按键动作触发,系统硬件层面的中断处理机制随即响应,这包括捕获键盘扫描码并将其转换为可识别的字符,这些字符随后被逐一填充至前述的输入缓冲区。此过程持续进行,直至用户敲击回车键,标志着一个完整输入序列的结束,此时,先前暂停的进程被唤醒。

继而,`getchar`继续其使命,它负责周期性地检查输入缓冲区。每当缓冲区中存在可用字符,`getchar`即迅速提取之,供程序进一步处理;反之,若缓冲区仍旧空空如也,则重复之前的等待流程。这一系列操作,不仅揭示了从物理按键到程序可用字符的转换路径,也深刻体现了操作系统在管理输入输出时的高效调度策略与事件驱动机制,确保了用户输入与程序处理间的无缝衔接。

8.5本章小结

在本章节的探讨中,我们深入解析了Unix系统中I/O设备管理的核心理念与机制。该系统巧妙地将各类I/O设备抽象成文件的形式,这一设计哲学不仅极大地统一了数据与设备访问的接口,还深化了对“一切皆文件”这一原则的理解与应用。在此基础上,对I/O设备的操作,诸如文件的开启与关闭、数据的读写、以及文件指针的定位调整等,均转化为对文件操作的直接映射,极大地简化了系统调用的复杂度与用户的认知负担。

进而,我们通过剖析`printf`与`getchar`这两个基础而又典型的I/O函数,揭示了这一抽象层次下数据流动的微观机制。`printf`展示了数据如何从程序内部构造,经由缓冲、格式化,最终穿越系统调用的边界,到达输出设备的细腻过程。而`getchar`则逆向揭示了输入数据自用户键盘敲击起始,经历中断处理、字符编码转换,到填充缓冲区,直至被程序获取的完整链路。这一系列分析不仅加深了我们对I/O设备管理机制的技术理解,还凸显了操作系统在数据交换与设备交互中扮演的中介与调度角色,以及其如何通过高度抽象化的设计,实现复杂功能的同时,保持了接口的简洁与一致。

(第81分)

结论

通过上述八章的详尽解析,我们得以全面洞察一个看似简单的“Hello, World!”程序背后所蕴含的复杂技术架构与操作系统原理。这一过程,不仅是代码的编译与执行之旅,更是对计算机科学底层机制的一次深度探索。

从最开始,程序员以纯文本形式在编辑器中编写源代码,到预处理器通过宏定义和头文件包含等操作对代码进行初步加工,源码逐渐丰富并具备编译前的完备性。随后,编译器接手,将高级语言的逻辑转化为汇编语言,标志着理解层次从人类可读向机器可解的过渡。汇编器紧随其后,将汇编代码翻译成目标文件,为后续链接做准备,此阶段地址空间尚处待确定状态,预留了重定位的空间。

链接器的介入,将独立的目标模块与必需的库文件整合为一个可执行的整体,完成地址重定位,确保了代码与数据的正确引用。操作系统通过Shell的fork与execve机制,优雅地创建并配置进程,利用虚拟内存系统将程序映射至进程地址空间,为执行奠定基础。同时,进程的虚拟地址空间与内存管理机制,如分段与分页,确保了高效、安全的内存访问。

在程序执行期间,它不仅要应对诸如键盘中断、停止信号等异步事件,还需通过中断与I/O端口等机制,与外部硬件设备进行紧密互动,体现了操作系统在协调软硬件交互中的核心作用。直至程序运行至终,无论是正常退出还是因信号中断而终止,操作系统都会负责清理战场,回收资源,确保系统资源的有效循环利用。

这一系列环环相扣的操作,不仅揭示了计算机系统内部机制的精妙与复杂,更是对数十年来无数工程师智慧结晶的致敬。正是他们构建的这座坚不可摧的桥梁,使得程序员能够在高层抽象上自由创作,而无需深陷底层技术的泥潭。这一桥梁,以其卓越的隐蔽性和高效性,默默支撑着现代计算世界的繁荣,让技术与创意得以无限延伸。

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

附件

hello.c:源代码

hello.i:预处理之后的文件

hello.s:编译之后的文件

hello.o:汇编之后的代码

hello: 可执行目标文件

hello.elf:hello的elf文件

helloo.elf:hello.o的elf文件

hello.txt:通过对hello反汇编生成的代码

helloo.txt:通过对hello.o反汇编生成的代码

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

参考文献

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

[1]  林来兴. 空间控制技术[M]. 北京:中国宇航出版社,1992:25-42.

[2]  辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.

[3]  赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).

[4]  谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.

[5]  KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.

[6]  CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值