HIT-CSAPP大作业-程序人生

打印 上一主题 下一主题

主题 2064|帖子 2064|积分 6192

 
第1章 概述

1.1 Hello简介

根据Hello的自白,利用计算机体系的术语,简述Hello的P2P,020的整个过程。
P2P即“From Program to Process”,就是从程序文件变为进程的过程。起首源代码将写入hello.c文件中,即为“Program”,在运行该程序时,要举行预处置惩罚生成hello.i文件,然后通过编译将C代码转换为汇编代码写入hello.s,再将汇编代码转换为机器码,生成可重定位目标文件hello.o,末了通过链接生成可实行目标文件hello。此时当用户再Shell输入./hello时,会调用fork()和execve(),内核加载hello到内存,创建进程并实行,即为“Process”。
020即“From Zero-0 to Zero-0”,这一过程中包括了从无到有再到无。最初内存状态中无hello程序的代码数据等,当用户再Shell输入./hello后,会调用fork()创建子程序,execve()调用子程序,其代码和数据会被加载到内存而后实行。程序制止时,内核将释放内存页,父进程回收子程序资源,内存中不再由关于hello的相关内容,变为无。
 
1.2 情况与工具

列出你为编写本论文,折腾Hello的整个过程中,利用的软硬件情况,以及开辟与调试工具。
硬件情况:处置惩罚器13th Gen Intel(R) Core(TM) i9-13900H
RAM:32.00GB
体系类型:64 位操作体系, 基于 x64 的处置惩罚器。
软件情况:Windows11,Ubuntu20.04
开辟与调试工具:visual studio,gcc as,ld,gdb,edb,gedit,readelf
1.3 中间结果

列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
在本论文中生成的中间文件如下表所示
文件名
文件作用
文件内容
hello.c
C 源代码文件
程序员编写的 C 语言源代码
hello.i
预处置惩罚后的代码
睁开宏、头文件(#include)、条件编译等,生成纯 C 代码
hello.s
汇编代码文件
由 C 代码翻译成的汇编指令
hello.o
目标文件(二进制)
机器码(二进制)
hello
可实行文件
链接全部目标文件和库后生成的完备可实行程序
表1.1 中间文件名称及作用

1.4 本章小结

在第一章中重要介绍了hello的p2p,020过程的内涵,说明了本论文的实验情况,包括软硬件情况以及开辟与调试的工具,末了列出实验过程中产生的中间文件和结果,对本实验有着总梳理的作用。
 
第2章 预处置惩罚

2.1 预处置惩罚的概念与作用

1.预处置惩罚的概念
预处置惩罚是 C/C++ 编译过程的第一步,由预处置惩罚器在正式编译之前对源代码举行处置惩罚,使原始的 .c文件hello.c经过宏睁开、头文件插入等处置惩罚后生成纯C代码文件hello.i。
2.预处置惩罚的作用
预处置惩罚的作用有以下几点,起首处置惩罚以 # 开头的预处置惩罚指令,如#include,#define等;其次可以删除注释,全部 // 和 /* ... */ 注释会被移除,生成干净的代码;再次举行宏睁开,将宏替换为定义的值或代码片段;接着可以添加行号和文件名标记,用于编译器报错时定位原始代码位置;末了可处置惩罚特殊指令。通过一系列操作末了生成 .i后缀的文件。
2.2在Ubuntu下预处置惩罚的命令

在Ubuntu体系下,举行预处置惩罚的指令是
gcc -m64 -Og -no-pie -fno-stack-protector -fno-PIC hello.c -o hello。
截图如下

图2.1 预处置惩罚指令

2.3 Hello的预处置惩罚结果分析

对hello.c举行预处置惩罚后生成hello.i文件,该文件内容大量增加。开头这些是预处置惩罚器插入的标记(如 # 1 "hello.c"),表示源文件的行号和文件名,用于调试和错误定位。举行了头文件睁开处置惩罚,头文件被替换为实际内容,包含大量体系级声明,同时函数例如printf 被替换为具体的 extern 声明,来自 stdio.h 的睁开。末了用户的代码被保留且注释已被删除。


图2.2 预处置惩罚部分结果

2.4 本章小结

本章介绍了预处置惩罚所举行的工作,包括头文件睁开,宏替换,删除注释等,并且在Ubuntu体系下对hello.c文件经过预处置惩罚后的hello.i文件举行了分析。
 
第3章 编译

3.1 编译的概念与作用

1.编译的概念
编译指将通过编译器将预处置惩罚后的源代码(.i文件)转换为汇编代码(.s文件)的过程,是C/C++程序构建的第二步。
2.编译的作用
编译会对代码举行语法语义分析,在举行词法分析是会将其分解为Token,语法语义分析会查抄其精确性以及验证类型是否匹配、变量是否声明等逻辑错误。然后生成中间表示便于优化,末了举行代码优化,如删除死代码、循环睁开等,而后即可生成汇编代码,为后续转换为机器码做准备。       

3.2 在Ubuntu下编译的命令

编译过程为由hello.i文件生成hello.s文件,在Ubuntu情况下命令为
gcc -S hello.i -o hello.s。

图3.1 编译命令实行

3.3 Hello的编译结果分析

3.3.1 数据
1.常量
字符串常量通常存储在.rodata段,如图,前者为由UTF-8编码的中笔墨符串,后者为格式化字符串,用于printf的输出。

图3.2 字符串存储

数字常量大多存储在.text段,其中包含与argc比较时利用的数字,比较循环变量是否符合循环条件的数字,还有设置的返回值0等。

图3.3 数字常量存储

2.变量
局部变量重要存储在栈中或某一个临时寄存器中,在源代码中重要存在三个局部变量,包括argc,argv,i。
对于argc,其存储在栈中-20(%rbp),是main的第一个参数,用于查抄用户是否输入了精确的参数数量,如果argc != 5,则打印错误信息并退出。

图3.4 argc的存储

对于argv,其存储在栈中-32(%rbp),是main的第二个参数,用于访问用户输入的参数。

图3.5 argv的存储

对于i,其存储在栈中-4(%rbp),作为循环计数器,控制循环实行10次。

图3.6 i的存储

3.3.2 赋值操作
1.直接赋值=
对于循环变量i,对其举行初始化赋值i=0,也是赋初值操作,同时return 0设置返回值也是。


图3.7 赋初值操作

2.运算后赋值
在举行循环时,每次循环后举行i++ 操作,即先+1后付给i。

图3.8 运算后赋值

3.不赋初值
对于argv,其直接传入栈中利用,未清零。

图3.9 不赋初值

3.3.3 算术操作
对于循环计数器i,在每一次循环后举行自增操作。
 


图3.10 自增运算

3.3.4 关系操作
对argc举行判断,判断其是否与5相称。还对i举行变量查抄i<=9,从而判断是否继续举行循环。
 


图3.11 关系操作

3.3.5 数组/指针操作
对于argv[] 运用数组,存储对应用户输入的参数,同时解引用argv[] 的指针。

 

图3.12 数组/指针布局

3.3.6 控制转移
在本代码中,运用了if条件判断以及for循环。
3.3.7 函数操作
Main函数,通报其参数argc和argv,其返回值存储在寄存器%eax中并设置为0。
 

图3.13 main函数


Printf函数,调用的时候传入字符串参数首地址,在循环中传入了argv[]五个参数的地址,输出用户信息。
 

图3.14 printf函数

Sleep函数,for循环后都会调用一次,用于暂停指定秒数。
 

图3.15 sleep函数

Atoi函数,for循环后都会调用一次。
 

图3.16 atoi函数

Exit函数,退出程序。
 

图3.17 exit函数

3.4 本章小结

本章对编译的概念和作用举行了具体说明,并联合在Ubuntu情况下举行编译的情况,说明本次实验中存在的C语言的数据与操作,数据上包含常量、变量,操作中运用了部分算数操作、关系操作、数组、控制转移以及函数操作。
 
第4章 汇编

4.1 汇编的概念与作用

1.汇编的概念
汇编是将可读的汇编代码(.s文件)转换为机器可实行的二进制指令,将这些指令打包形成一个可重定位目标程序的格式,将结果文件保留在.o文件中,该过程由汇编器完成。
2.汇编的作用
在本过程中,利用汇编器处置惩罚 .s文件,举行词法语法分析后将汇编指令转化为机器码,然后举行符号分析,定义符号并记录其地址,标记未分析符号,留待链接器处置惩罚,末了生成目标文件格式,通常Linux下位ELF格式,其中包括 .text段, .data段,符号表和重定位表等。这为链接器生成终极可实行文件奠定基础。
4.2 在Ubuntu下汇编的命令

在Ubuntu下,汇编指令运用as hello.s -o hello.o。截图如下:

图4.1 汇编命令

4.3 可重定位目标elf格式

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

4.3.1 文件头
通过readelf相关指令起首查看ELF头文件,ELF 文件头是 ELF 文件的起始部分,它包含了关于文件的根本信息,其重要内容包括文件类型、架构、版本、入口点地址、程序头表偏移、节头表偏移、标志、头巨细、程序头表项巨细、程序头表项数量、节头表项巨细、节头表项数量、节头表字符串表索引。
文件头是为工具(如链接器、加载器和调试器)提供必要的信息,以便精确分析和处置惩罚 ELF 文件。它为后续的节头表和程序头表的分析提供了入口点,并确保文件的布局和内容符合预期。

图4.2 ELF文件头


4.3.2 全部节
该部分展示了全部节的名称、类型、地址、偏移量、巨细等信息。

图4.3 全部节

4.3.3 重定位表
该表记录了hello.o中全部必要链接器修正的地址引用,引导链接器如何将符号绑定到实际内存地址。每行中包含的字段有偏移量,信息,重定位方式类型,符号值,符号名称以及加数。
其中R_X86_64_PC32是对相对地址的32位修正,用于访问 .rodata 中的字符串。R_X86_64_PLT32是对过程链接表(PLT)的32位修正,用于调用外部库函数。对于.eh_frame节,其作用是修正异常处置惩罚帧中对 .text 节起始地址的引用。
通过分析重定位表,可以加深理解程序如何从“未链接的 .o 文件”变为“可实行的完备二进制”。


图4.4 重定位表

4.3.4 符号表
符号表是ELF文件的核心数据布局之一,记录了全部符号(函数、变量、节等)的定义和引用信息,为链接器提供关键绑定依据。在本符号表中,Num1和10为已定义符号,11-17为未定义符号必要链接器分析。符号表指出必要什么共同重定位表工作,重定位表则指出在那里修正地址。

图4.5 符号表

4.4 Hello.o的结果分析

以下为实行命令行后查看反汇编代码的截图:

图4.6 反汇编代码

反汇编于hello.s的对照分析如下:

  • 栈帧初始化阶段:汇编指令直接转换为对应的机器码。
  • 操作数:汇编代码利用符号(如 .LC0、printf@PLT),机器码利用临时值,偏移量为 占位符0x0,重定位条目 R_X86_64_PC32 提示链接器修正为 .rodata 节的真实偏移。
  • 函数调用:对于未分析的符号,即全部外部函数调用(如 printf、sleep)和全局数据引用(如 .LC0)在 .o 文件中均为占位符00 00 00 00,就必要重定位类型 R_X86_64_PLT32 表示通过过程链接表(PLT)跳转修正。
  • 局部变量操作:汇编指令与机器码完全对应,无重定位需求。
机器语言由操作数和操作码构成,其中操作码为用两位的十六进制数表示一个操作,操作数包含立即数、寄存器或内存地址。机器语言中还会有重定位信息的出现,用于相对寻址和动态链接的函数调用。
下表为举例说明汇编指和机器语言之间的映射关系和差别。

表4.1 映射关系与差别

汇编指令

机器码

差别

push %rbp

55

无操作数,直接编码。

mov $1, %eax

B8 01 00 00 00

立即数内嵌在指令中。

lea .LC0(%rip),%rdi

48 8d 3d 00 00 00 00 + 重定位

符号地址需链接时修正。

call printf@PLT

e8 00 00 00 00 + 重定位

动态链接函数地址延长绑定。

jle .L4

7e 0a

偏移量由汇编器计算。


4.5 本章小结

本章中说明了汇编的概念和作用,在Ubuntu下举行实验,查看了ELF文件的各个节,尤其对重定位节和符号节举行深入解释说明,同时,利用objdump指令举行反汇编代码查看,把握机器语言布局组成,对比机器语言和汇编语言的相互映射关系以及表示方法上的差别。
 
第5章 链接

5.1 链接的概念与作用

1.链接的概念
链接是将多个目标文件(.o)和库文件,包括静态库或动态库,归并为一个可实行文件的过程,由链接器完成。
2.链接的作用
链接的过程中,起首举行符号分析,解决未定义符号,将目标文件中的未定义符号绑定到库中的实际定义。然后举行地址重定位,修正代码和数据地址,根据重定位表,将临时占位符替换为实际地址。接着归并代码与数据段,将各目标文件的 .text、.data、.rodata 等段归并,分配终极的内存地址。末了处置惩罚静态/动态库,对于静态链接,将库代码直接复制到可实行文件中,对于动态链接,在运行时加载共享库,淘汰磁盘和内存占用。
5.2 在Ubuntu下链接的命令

在Ubuntu下举行链接的命令为:
ld -o hello -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o hello.o/usr/lib/x86_64-linux-gnu/libc.so/usr/lib/x86_64-linux-gnu/crtn.o

图5.1 链接命令

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

分析hello的ELF格式,用readelf等列出其各段的根本信息,包括各段的起始地址,巨细等信息。
5.3.1 文件头
该文件头是ELF文件的实在,其重要内容包括文件类型、架构、版本、入口点地址、程序头表偏移、节头表偏移、标志、头巨细、程序头表项巨细、程序头表项数量、节头表项巨细、节头表项数量、节头表字符串表索引。

图5.2 文件头

5.3.2节头表
该表中描述了各段的巨细,地址以及偏移量等信息,能够清楚直观地获取有用信息。其中.text段地址为0000000000000280,巨细为000000000000010f,.rodata段地址为00000000000003a4,巨细为0000000000000008,.data段地址为0000000000000600,巨细为0000000000000048。

图5.3 节头表截图

5.4 hello的虚拟地址空间

利用edb打开实行文件hello,调试器会分析 ELF 文件,加载代码和数据段到内存。在edb界面中,可通过Data Dump查看加载到虚拟地址的程序代码,具体内容如下图所示。

图5.4  Data Dump界面

由图可知,ELF文件起始地址为0x400000,且有ELF标识,与5.3节中节头表对比可知,.init段由地址0x401000起始,.text段由地址0x4010f0起始,.rodata段由地址0x402000起始,.data段由地址0x404048起始,截图如下。




图5.5 部分节对应虚拟地址查看



    利用gdb/edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。  
5.5 链接的重定位过程分析

利用指令objdump -d -r hello举行反汇编查看。
在hello.o文件中,还未举行链接操作,对于未分析的符号,其指令地址为占位符00 00 00 00,存在重定位类型如 R_X86_64_PLT32 和 R_X86_64_PC32,提示链接器必要修正的地方。
在hello的反汇编文件中,地址都已修正,全部00 00 00 00被替换为实际地址,并且已无重定位标记,链接器已完成全部符号绑定,可直接实行文件。如下图所示为部分反汇编代码。

图5.6 部分hello文件反汇编

在经过二者的对比后,我们举行总结说明链接的过程。链接是将多个.o目标文件和库文件归并生成可实行文件,重要包括符号分析和地址重定位两个核心步骤。起首,链接器扫描全部目标文件的符号表,将未定义的符号绑定到库中的实际定义;然后根据重定位表,将指令中的临时占位地址(如00 00 00 00)修正为终极的绝对地址或相对偏移。同时,链接器会集并代码段、数据段等,分配虚拟内存地址,并处置惩罚动态链接库的延长绑定(通过PLT/GOT机制)。终极生成的可实行文件hello不再包含未分析符号,可直接加载运行。
对于hello中重定位的具体分析如下,举行相关举例说明。
对于数据的引用修正,即R_X86_64_PC32类型,通过计算,偏移量=.rodata地址-下条指令地址-4。对于函数的调用修正,即R_X86_64_PLT32类型,经过重定位操作后,在初次调用 puts 时,跳转到 puts@plt处,PLT 通过 GOT(全局偏移表)延长绑定到 libc 中的实际函数。



图5.7 重定位过程分析


5.6 hello的实行流程


5.6.1. 动态链接初始化阶段
在Hello程序实行的初始化阶段,起首由动态链接器入口_dl_start完成关键准备工作,包括初始化全局偏移表(GOT)和加载程序依赖的全部共享库。随后跳转至_dl_init函数,该函数负责分析动态符号表内容并对关键函数地址举行重定位,特别是尺度库函数的PLT条目。完成动态链接初始化后,程序转入由链接器自动生成的入口点_start,该入口点负责设置初始栈帧布局和寄存器状态,为程序实行做好准备工作。
通过objdump -f hello查看入口地址,程序从0x4010f0开始实行,对应_start函数。

图5.8 查看入口地址

5.6.2. 运行时情况配置
程序调用位于libc库中的__libc_start_main函数举行运行时情况配置,这一过程包括三个核心步骤:初始化线程局部存储(TLS)区域、建立情况变量表(environ)、以及注册退出处置惩罚函数机制。其中,通过__cxa_atexit函数登记程序制止时必要实行的资源清理函数,包括全局对象析构和IO流关闭等操作。
通过gdb查看,可知利用callq指令,通过PLT跳转调用_libc_start_main。

图5.9 查看界面

5.6.3. 静态数据初始化
完成运行时情况配置后,程序继续实行静态数据初始化,调用__libc_csu_init函数实行静态构造过程,具体包括实行.init_array段中的初始化函数、调用C++全局对象的构造函数,以及初始化尺度IO流(stdin/stdout/stderr)。
程序调用libc提供的_setjmp函数生存当前实行情况上下文,为后续可能发生的异常处置惩罚和信号规复建立基础。这一系列初始化步骤环环相扣,确保Hello程序获得完备的实行情况,为main函数的正常实行做好充分准备。
5.6.4. 主程序实行阶段
进入 main 函数,用户代码开始运行,对参数举行查抄,hello中直打仗发退出,有用参数情况下通过PLT机制的输出处置惩罚。当main函数实行结束后,程序将将调用exit()来结束进程。程序结束要实行已注册的退出处置惩罚函数,释放动态链接资源,发起体系调用制止进程。

利用gdb/edb实行hello,说明从加载hello到_start,到call main,以及程序制止的全部过程(重要函数)。请列出其调用与跳转的各个子程序名或程序地址。
5.7 Hello的动态链接分析

5.7.1 共享库函数地址的动态特性
在当代计算机体系布局中,应用程序调用动态链接库(例如尺度C库)中的函数时,编译过程无法确定这些函数终极的内存位置。这种动态寻址特性重要由以下因素决定:
1.地址空间布局随机化(ASLR):安全机制使得操作体系将共享库载入随机的虚拟内存区域
2.跨进程共享机制:相同的共享库可能被差别进程利用,但在各自虚拟地址空间中映射位置差别
3.运行时动态加载:部分库可能通过动态加载接口在程序运行期间才被载入内存
5.7.2 延长分析技术方案
为应对函数地址不确定的挑衅,当代编译体系接纳延长分析机制,其核心架构包含两大组件:
1. 全局偏移表(GOT)布局
全局偏移表(GOT)作为基础数据布局,位于.got.plt段中,在x86-64平台上每个单位占用8字节空间。GOT接纳分层设计:首单位存储动态段(.dynamic)地址信息,次单位生存link_map布局指针,第三单位指向动态分析函数_dl_runtime_resolve,后续单位则作为各函数实际地址的缓存区,初始时指向分析桩代码。
2. 过程链接表(PLT)布局
过程链接表(PLT)布局,它由.plt段中的指令序列组成,每个函数入口占用16字节空间。PLT接纳三级架构:首入口作为特殊跳板负责触发动态分析流程,次入口处置惩罚体系初始化函数(如__libc_start_main)的跳转,通例入口则为各库函数提供跳转署理。
这一机制的实际工作流程表现为:当程序初次调用函数时,会先经过PLT桩代码,此时GOT中对应项仍指向分析器;动态链接器随即到场分析实际函数地址,并更新GOT表项;后续调用时则可以直接通过GOT跳转到目标函数。
对于hello程序,查看动态链接前后的变化,起首通过readelf -S hello查看.got .plt节的内容。

图5.10 查看内容 

利用edb举行调试可见下图:
 

图5.11 实行init之前的地址


图5.12  实行init之后的地址

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

 在第五章中介绍了链接的概念与作用,体系性地探究了程序链接的核心机制及实在现原理,并且得到了链接后的hello可实行文件的ELF格式文本,分析了与hello.o的ELF格式文本的区别,通过两个反汇编文件的比较,加深了对于重定位和动态链接的理解。既从理论层面阐释了链接的概念与功能价值,随后又通过实验本领对可实行文件的生成过程举行了实证研究。
 
第6章 hello进程管理

6.1 进程的概念与作用  

1.进程的概念
进程是操作体系举行资源分配和调度的根本单位。每个进程拥有独立的地址空间(代码、数据、堆栈)、文件描述符、寄存器状态等资源,由操作体系内核通过进程控制块(PCB)管理。进程在运行时表现为一个正在实行的程序,其周期包括创建、调度、实行和制止。
2.进程的作用
进程的核心作用是实现多使命并发实行和资源隔离。通过进程机制,操作体系可以同时运行多个程序,每个进程独立运行且互不干扰,当体系调用制止时结束。进程还提供很多功能,如内存保护,防止越界访问,错误隔离,当一个进程崩溃时不会影响其他进程等。
6.2 简述壳Shell-bash的作用与处置惩罚流程

1.Shell的作用
Shell是操作体系的命令行解释器,充当用户与内核之间的桥梁,下面将介绍其重要功能。起首可以举行命令分析与实行,吸收用户输入的命令,分析后调用对应的程序或体系调用。其次可举行脚本运行,实行 Shell 脚本即.sh 文件,从而实现自动化使命。还对情况举行管理,维护情况变量,控制进程的工作目次、输入输出重定向等。再次举行作业控制,管理前台和背景的其他进程。末了它还有其他扩展功能,能够支持通配符(*)、管道(|)、条件判断等编程特性。
2.Shell的处置惩罚流程
以实行命令 ./hello 为例,Bash 的处置惩罚流程如下:
起首读取用户输入的命令,从终端或脚本读取命令 ./hello,按空格分割为单词。然后实行分析命令,查抄是否为内置命令,没有则视为外部程序。若命令举动路径(如 ./hello),直接尝试实行;若为命令名(如 ls),通过 PATH 情况变量查找可实行文件。而后调用 fork() 复制当前 Shell 进程,生成子进程。子进程通过 execve() 体系调用hello的代码和数据,替换自身内存空间。若命令是前台运行,Shell 调用 wait() 壅闭,直到子进程结束;若为背景,立即返回提示符。子进程的 stdout/stderr 将输出到终端,或被重定向到文件。当其退出后,Shell 获取其退出状态,并打印提示符等候下一条命令。
6.3 Hello的fork进程创建过程

fork()体系调用就是创建一个与父进程近似完全相同的子进程,形成父子关系,下面将对其完备流程举行分析。起首用户输入./hello后,Shell分析出需实行的可实行文件路径。然后Shell调用fork()创建子进程,内核分配新的进程描述符PCB,子进程获得唯一的PID,父进程ID(PPID)设为Shell的PID。子进程对父进程的地址空间、文件描述符表、资源限制设置举行复制,并且继承父进程的信号处置惩罚设置。同时CPU举行上下文复制,复制父进程的寄存器上下文、父进程的内核栈,设置子进程的程序计数器(PC)与父进程相同。进程关系建立后,会举行特殊的返回值处置惩罚在,fork()在父进程中返回子进程PID,在子进程中返回0。
对于父进程来说,若命令为前台使命,调用waitpid()壅闭,等候子进程结束。若为背景使命,立即返回提示符,子进程由init进程接管。对于子进程来说,将独立运行,拥有本身的内存空间和资源,实行完毕后通过exit()制止,内核回收其资源。
当代操作体系实现fork()时接纳写时复制技术提高效率,初始时父子进程共享相同的物理内存页,并且内核将这些内存页标记为只读,当任一进程尝试写入共享页时,触发页错误,内核此时才真正复制被修改的页,为写入进程创建私有副本。这种机制制止了不必要的内存复制,大大提高了fork()的性能

  • 子进程实行execve()加载Hello:子进程调用指令execve("./hello", argv, environ),内核销毁子进程原有的地址空间,加载hello的ELF文件,对.text、.data等段举行初始化。程序计数器PC设置为指向_start入口,终极跳转到main()。
  • 父子进程分离实行:
6.4 Hello的execve过程

当Shell通过fork()体系调用创建子进程后,会进一步利用execve()系列函数来加载并实行新的目标程序(例如hello),该操作将彻底重修进程的实行上下文。
起首,execve函数体系调用必要吸收三个核心参数。
(1)文件标识,即目标可实行文件的完备路径,通常由Shell通过PATH情况变量分析获得。
(2)参数列表,为以NULL末端的命令行参数字符串数组,其中首元素为程序名称本身。
(3)情况变量,包含进程运行情况信息的键值对集合新进程将继承这些情况设置。
    必要注意的是,该调用在成功实行时不会返回(因为原进程映像已被完全替换),仅在目标文件无法访问等错误情况下才会返回错误状态。
接下来具体阐述内存空间重修的流程,具体步骤如下。
(1)原空间清理阶段,将解除当前进程全部用户态内存区域的映射关系,包括代码区、数据区、堆栈段等。
(2)新空间构建阶段,建立目标程序代码段的私有映射(接纳写时复制技术优化性能);对数据段举行初始化操作,加载已初始化的数据段(.data),清零未初始化数据段(.bss);创建新的栈内存区域,包含命令行参数、情况变量以及辅助向量;完成动态加载通过动态链接器(ld.so)映射程序依赖的全部共享库对象;举行控制权转移,将实行流程跳转到新程序的初始化入口点(通常为_start符号)。
整个加载过程接纳了多项优化技术,写时复制(COW)机制,在代码段映射时设置私有标志,仅在修改时触发实际拷贝;惰性加载,动态链接库的实际加载延长到初次函数调用时;情况继承,新进程会继承父进程的文件描述符表(除非显式设置FD_CLOEXEC标志)。这一系列技术共同确保了进程创建和实行的高效性,使得Unix/Linux体系能够快速相应各种程序实行哀求,同时保持体系资源的公道利用。
实行流程示意图如下所示:
[Shell进程] → fork() → [子进程副本] → execve() → [内存重构] → [_start入口] → [新程序实行]
6.5 Hello的进程实行

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

当hello进程被Shell通过fork()和execve()加载后,其实行过程涉及操作体系多个子体系的协同工作,特别是进程调度和状态转换机制。以下是这一过程的具体分析。
1. 进程调度与时间片管理
Linux内核接纳CFS(完全公平调度器)算法管理进程实行,hello进程被创建时,CFS会为其分配一个初始时间片,在CPU密集型场景下,hello进程会连续消耗CPU时间,时间片耗尽时,硬件时钟(IRQ 0)会触发中断,通知调度器到场。调度器根据进程的vruntime值选择下一个要实行的进程,因此hello进程可能被重新放入停当队列,等候下次调度。
2. 进程状态转换与上下文管理
hello进程在实行过程中会经历多种状态转换,如下所示。


[停当队列] --调度--> [运行态] --时间片耗尽--> [停当队列]

                   |__体系调用--> [内核态]

                   |__信号中断--> [就寝态]


状态发生转换有以下几种情况,可能主动让出CPU,通过体系调用进入内核态,大概被动抢占,时间片耗尽或被更高优先级进程抢占,又或是存在等候事件,因I/O操作或信号处置惩罚进入就寝态。

当发生进程切换时,内核会完备生存当前实行上下文,包括通用寄存器、程序计数器(%rip)、栈指针(%rsp)、状态寄存器等,这些信息被存储在进程控制块(PCB)中。

3. 用户态与内核态转换

hello进程在实行过程中会频仍经历用户态和内核态的转换。核心态操作类型有以下几种,内存管理,可处置惩罚缺页异常(如访问未映射的虚拟地址)并管理写时复制页面;装备I/O,控制终端输出(printf终极通过write体系调用实现),处置惩罚硬件中断;控制进程,相应信号(如SIGINT处置惩罚)等。


6.6 hello的异常与信号处置惩罚

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

表6.1异常根本类型

异常类别
异步/同步
缘故起因
返回举动
中断
异步
来自I/O装备的信号
总是返回到下一条指令
陷阱
同步
有意的异常
总是返回到下一条指令
故障
同步
埋伏可规复的错误
可能返回到当前指令
制止
同步
不可规复的错误
不会返回
下图为上述根本异常类型的处置惩罚方法:
 

图6.1 中断异常处置惩罚


图6.2陷阱异常处置惩罚


图6.3故障异常处置惩罚


图6.4 制止异常处置惩罚

表6.2 异常与信号类型分析

异常类型
产生信号
处置惩罚方式
实验验证方法
键盘中断
SIGINT
制止进程(Ctrl+C)
strace -e signal ./hello
终端制止
SIGTSTP
挂起进程(Ctrl+Z)
jobs命令查看
非法内存访问
SIGSEGV
核心转储(示例代码人为制造段错误)
gdb分析core文件
算术异常
SIGFPE
制止进程(示例除零错误)
添加int a=1/0;代码
以下为程序运行时举行差别键盘输入时的运行状态。
1.不停乱按
当程序运行过程中不断乱按,包括回车,此时不会对程序有影响,在正常运行。

图6.5 程序运行时不断乱按

2.按ctrl-z
向程序发送信号,吸收信号后作业将被挂起,但不会被回收。通过ps指令,可看到PID为6262的hello进程仍在运行。

图6.6 程序运行时按ctrl-z


图6.7 ps指令查看


3.按ctrl-c
实行jobs指令并调用fg,将背景运行或挂起的作用放到前台终端运行。输入ctrl-c想前端发送SIDINT信号,制止作业。
 

图6.8 指令实行

4.按回车
在实行过程中不断按回车,程序正常举行,但在运行结束后会出现等宽的空行。回车的信息发送到了shell中,同样实行。

图6.9 按回车

6.7本章小结

这一章介绍了进程的概念与作用,体系地分析了hello程序的完备生命周期,从进程创建到制止过程中所涉及的关键机制,描述了shell的作用与处置惩罚流程,描述了hello的fork进程创建过程以及execve过程,描述了进程上下文信息、用户模式内核模式、进程调度过程等,末了说明了hello的异常以及信号处置惩罚过程。
 
第7章 hello的存储管理

7.1 hello的存储器地址空间

联合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
1.逻辑地址
逻辑地址(Logical Address)是程序在编译和链接阶段生成的地址。它是CPU在程序运行时生成的地址,由段选择符和段内偏移量组成,存在于指令或操作数中。逻辑地址的特点是与硬件无关,完全由程序逻辑决定,因此差别进程可以利用相同的逻辑地址而不会辩论。在分段机制下,逻辑地址必要通过段式内存管理单位(MMU)转换为线性地址。
逻辑地址的核心意义在于为程序提供统一的地址空间,屏蔽底层物理内存的复杂性,使得每个进程都认为本身独占内存资源。例如,在x86架构中,逻辑地址表现为“段:偏移”的情势,而当代操作体系通常将段基址设为0,使逻辑地址直接等同于偏移量,简化了地址转换流程。
在hello中的变量地址或函数指针即为逻辑地址,通过反汇编查看时,其逻辑地址必要通过计算得到,利用看到的地址加上对应段的基地址得到。
2.线性地址
线性地址(Linear Address)是逻辑地址经过分段机制转换后的中间地址,在分页机制启用前,它可直接对应物理地址;若启用分页,则需进一步通过页表转换为物理地址。线性地址的特点是连续且平坦,形成一个从0到最大限度的线性空间(如32位体系为4GB)。它是CPU的段单位输出、页单位的输入,充当分段与分页机制的桥梁。在大多数当代操作体系中,分段机制被弱化(如利用平坦模式),逻辑地址几乎直接映射为线性地址。
线性地址的核心作用是为分页机制提供统一的输入,使得操作体系可以通过页表动态管理物理内存。例如,Linux体系默认接纳分页机制,线性地址空间被划分为用户态和内核态两部分(如3:1划分),通过页表实现隔离与共享。
在hello中,程序中的符号在举行链接时确定的地址就是线性地址,如main对应的地址时0x400440。
3.虚拟地址
虚拟地址(Virtual Address)是程序视角下的内存地址,通常与逻辑地址同义,但在某些架构(如ARM)中特指通过MMU转换前的地址。虚拟地址的特点是独立于物理内存布局,构成每个进程的私有地址空间,使得程序无需考虑物理内存的实际分配。操作体系通过MMU将虚拟地址转换为物理地址,期间可能涉及分段、分页或段页式混淆机制。
虚拟地址的核心优势是提供内存隔离(进程间无法直接访问彼此内存)和扩展性(通过页面互换支持大于物理内存的地址空间)。例如,在64位体系中,虚拟地址空间可达2^64字节(实际实现可能缩减),但仅少量被映射到物理内存,其余部分通过缺页中断按需分配。
当Hello程序运行时,操作体系为其分配一个虚拟地址空间,每个进程都有独立的虚拟地址空间,通过反汇编可以查看。
4.物理地址
物理地址(Physical Address)是实际存储在内存硬件上的地址,由内存控制器直接利用。其特点是唯一且直接可寻址,全部终极访问都必须通过物理地址完成。物理地址与虚拟地址的映射关系由操作体系通过页表管理,对程序透明。物理地址空间通常小于虚拟地址空间(如32位体系最大4GB),且可能包含空洞,如保留给装备的区域。
其核心意义是解耦程序与硬件,使得操作体系能机动分配物理页(如页面置换、NUMA优化)。例如,在多核体系中,差别CPU核心可能通过物理地址共享同一内存,而虚拟地址则保障各进程的隔离性。

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

Intel处置惩罚器在保护模式下利用段式管理机制将逻辑地址转换为线性地址,这一过程涉及段选择符和段描述符的共同。起首举行几个概念的阐述。
1. 逻辑地址的构成
逻辑地址由两部分组成,段选择符和偏移量。段选择符为16位,存在于段寄存器(如CS、DS、SS等)中。偏移量在保护模式下为32位,由指令或寻址方式生成。逻辑地址的标识格式为段选择符:偏移量,如CS:EIP。
2. 段选择符的布局
已知段选择符是一个16位的字段,其中包含的信息如下。Index占有13位,是用于指向全局描述符表(GDT)或局部描述符表(LDT)中的段描述符条目。TI(Table Indicator)占有1位,置0表示利用GDT,置1表示利用LDT。RPL(Requested Privilege Level)占有2位,是哀求特权级,用于权限查抄。
3. 段描述符
段描述符是GDT或LDT中的一个8字节即64位的条目,定义段的属性,包括的内容有,段基址占32位,是段的起始线性地址;段边界占20位,表示段的巨细,其单位由粒度位决定;特权级占2位,是描述符特权级;类型占4位,表示段的类型,如代码段、数据段。别的还有其他标志,如是否可读/写、粒度位等。
下面将具体报告在Intel处置惩罚器的保护模式下,逻辑地址通过段式管理转换为线性地址的转换步骤。
起首根据段选择符定位段描述符,处置惩罚器根据TI位选择全局描述符表GDT或局部描述符表LDT。用Index × 8(因为每个描述符占8字节)计算描述符在表中的偏移地址。然后查抄权限和有用性,查抄段是否存在,即描述符的P位是否为1,以及哀求特权级(RPL)和当前特权级(CPL)是否满足DPL权限。接着从段描述符中提取32位的段基址。利用公式计算线性地址:线性地址 = 段基址 + 偏移量。如果启用分页管理(CR0.PG=1),线性地址还需通过页表转换为物理地址;否则,线性地址直接作为物理地址。
别的也存在一些的特殊情况,平坦模式时,当代操作体系通常将段基址设为0,段边界设为最大值(0xFFFFF),从而绕过段式管理,直接利用偏移量作为线性地址。分页启用时,线性地址会通过页表进一步转换为物理地址。
7.3 Hello的线性地址到物理地址的变换-页式管理

页式管理是当代操作体系实现虚拟内存的核心机制,它将程序的线性地址空间和物理内存划分为固定巨细的页(通常为4KB),通过多级页表建立映射关系,从而达到线性地址和物理地址的变换。
当Hello进程访问虚拟地址时,CPU的内存管理单位(MMU)起首从CR3寄存器获取顶级页表基址,然后依次查询PML4、页目次指针、页目次和页表四级布局,终极获得物理页框号并与页内偏移组合成物理地址。这一过程中,TLB(转译后备缓冲器)会缓存常用地址转换结果加速访问,若发生页缺失则触发缺页异常,由操作体系负责调入所需页面。
页式管理不仅实现了进程间的内存隔离,即每个进程拥有独立的页表,还通过写时复制(COW)、页面置换等战略优化内存利用效率,使得Hello程序在运行时可获得连续的虚拟地址空间,而实际物理内存则可能是分散的非连续存储。
7.4 TLB与四级页表支持下的VA到PA的变换

在x86-64体系布局中,虚拟地址(VA)到物理地址(PA)的转换通过硬件协同完成,具体装备如下。
MMU:内存管理单位,集成在CPU中,包含TLB和页表遍历逻辑。
TLB:Translation Lookaside Buffer,分为L1(64条目)、L2(1024条目)多级缓存。
四级页表:PML4→PDP→PD→PT四级布局,每级512个条目(9位索引)。
CR3寄存器:存储当前进程的PML4表物理基地址。
当Hello进程访问虚拟地址时,CPU起首查询TLB缓存是否存在该地址的转换记录;若TLB命中则直接获得物理地址。若未命中,则MMU从CR3寄存器获取顶级页表(PML4)基址,依次通过虚拟地址的47-39位索引PML4表项找到页目次指针表,38-30位索引PDP表项找到页目次(PD),29-21位索引PD表项找到页表(PT),末了用20-12位索引PT表项获得物理页框号,与12位页内偏移组合形成物理地址,同时将该映射存入TLB。
若任一页表项不存在,则触发缺页异常,由操作体系处置惩罚后再规复地址转换流程。整个过程完全由硬件自动完成,但对应用程序表现为透明的连续内存访问。
TLB和四级页表联合利用提高了虚拟地址到物理地址转换的效率。TLB提供快速缓存,制止频仍访问页表。若TLB未命中,体系通过四级页表渐渐分析虚拟地址,终极找到对应的物理地址。这种多级页表布局提高了内存管理的机动性和地址空间的利用效率。
7.5 三级Cache支持下的物理内存访问

当Hello程序访问内存时,CPU并非直接读写物理内存(DRAM),而是通过多级Cache(高速缓存)加速访问,下面将介绍相关内容。
1. Cache层级布局
当代x86-64 CPU通常接纳三级缓存布局。
L1 Cache:分指令(L1i)与数据(L1d)缓存,每个核心独享,延长1-3周期(通常32-64KB)。
L2 Cache:统一缓存,每个核心独享,延长约10周期(通常256KB-1MB)。
L3 Cache:全部核心共享,延长约30-50周期(通常2-32MB)。

图7.1 存储器三级存储布局

2. 物理内存访问流程
假设Hello程序读取变量int a,起首举行L1 Cache查询,CPU先用PA的低位地址索引L1d Cache组,对比标签位(PA的高位地址)验证是否命中,若命中,直接返回数据。L1未命中,再举行L2 Cache查询,查看核心私有的L2 Cache,接纳类似索引-标签机制,若命中:数据返回至L1d Cache并供CPU利用。L2未命中,则继续查询L3 Cache,多核间通过同等性协议(如MESI)维护数据,若命中,数据逐级添补L2→L1。Cache全线未命中就要访问主存,触发DRAM访问,通过内存控制器读取物理内存,数据按Cache Line为单位载入L3→L2→L1。若为写操作,可能接纳写归并或写回战略。
在cache三级内存访问过程中,接纳预取方式,CPU根据访问模式预测后续内存地址,提前加载至Cache,允许Cache未命中时继续实行其他指令,并且接受缓存同等性协议,多核情况下通过MESI协议维护Cache同等性,防止进程在差别核心运行时数据辩论。
7.6 hello进程fork时的内存映射

当Shell通过fork()创建Hello进程时,Linux内核接纳写时复制技术优化内存管理。根本内存布局复制分为两部分,页表复制,即子进程获得父进程(Shell)页表的完备副本,但全部用户空间物理页框暂时保持共享状态,仅将页表项标记为只读;虚拟地址空间继承,Hello进程继承父进程的完备内存布局(代码、数据、堆、栈等),包括.text段,.data/.bss段,堆空间,栈空间。
当父子进程任意一方尝试修改共享页时,MMU将触发缺页异常,因页表项标记为只读,CPU引发#PF异常。内核将分配新物理页,复制原页内容到新物理页,更新故障进程的页表项,指向新页并规复PTE_W权限,另一进程仍保留原物理页。
然后规复实行,进程继续实行写操作。
这种机制使得fork()只需复制页表布局,而非整个内存空间,极大提拔了进程创建效率,同时保证实际物理内存仅在写入时增长,显著提高内存利用率。
7.7 hello进程execve时的内存映射

当Shell通过execve()体系调用加载并实行Hello程序时,内核会完全重构进程的内存映射,具体过程如下。
1. 内核会销毁原内存空间,清除进程原有的全部用户空间内存映射,包括代码段、数据段、堆、栈等全部内存区域,释放对应的页表布局,但保留内核态资源。
2. 内核会根据Hello程序的ELF格式重新建立内存映射。将Hello可实行文件的.text段以只读+写时复制方式映射到内存,即使多进程运行同一程序也共享物理页。为数据段初始化,为.data段从文件映射为可写私有,已初始化全局变量,为.bss段分配归零页并标记为匿名私有映射。构建新栈空间,创建新的用户栈,压入命令行参数(argv)和情况变量(envp)。再举行共享库的加载,动态链接器(ld.so)映射所需的.so文件,并建立GOT/PLT表。
在此过程中,当代Linux内核会启用地址空间布局随机化(ASLR),随机化代码段、数据段、堆和栈的基地址,以加强安全性。终极,Hello进程获得一个全新的、隔离的虚拟地址空间,其内存权限也被严格设置,确保程序的安全实行情况。这一机制不仅保证了进程内存空间的独立性,还通过共享只读段(如代码段和库文件)显著提高了体系的内存利用率。
7.8 缺页故障与缺页中断处置惩罚

缺页故障(Page Fault)是当进程访问虚拟内存时,CPU发现目标页不在物理内存或访问权限不符时触发的硬件异常(x86架构中为#PF异常,中断号14),其处置惩罚流程是虚拟内存管理的核心机制。当Hello进程访问某个虚拟地址时,MMU起首查询页表,若发现页表项(PTE)的Present位=0(页未加载)、权限不符(如写只读页)或保留位异常,则会触发缺页中断,CPU生存现场后跳转至内核的缺页处置惩罚程序(如Linux的do_page_fault())。内核根据缺页缘故起因分类处置惩罚:对于合法访问(如COW页或初次访问的匿名页),内核从磁盘换入数据或分配新物理页,更新页表并重试指令;对于非法访问(如访问未映射区域或权限违规),则向进程发送SIGSEGV信号制止进程。具体处置惩罚流程可分为以了局景:
1.次要缺页,页已分配但未映射,内核直接建立物理页映射,无需I/O操作。
2. 重要缺页,页未驻留内存,需从互换分区或磁盘文件(如共享库)读取数据,涉及I/O操作,延长可达毫秒级。内核预读机制会提前加载相邻页。
3.写时复制(COW)缺页,当Hello进程修改从fork()继承的共享页时,内核复制物理页并更新页表,标记为私有。
4.权限缺页,访问只读页(如修改代码段)或用户态访问内核页会触发保护错误,直接制止进程。
内核处置惩罚完成后,会规复进程现场并重新实行触发缺页的指令。当代操作体系通过工作集模子和页缓存(Page Cache)淘汰缺页频率,例如Hello频仍访问的库函数页会被保留在活跃LRU链表。缺页机制不仅实现了按需加载、内存超分配等关键特性,还支持了COW、内存映射文件等高级功能,是虚拟内存体系的基石。
7.9动态存储分配管理

以下格式自行编排,编辑时删除
Printf会调用malloc,请简述动态内存管理的根本方法与战略。(此节课堂没有教学,选做,不算分)
7.10本章小结

在本章中,重要介绍了hello的存储地址空间,intel的段式管理,hello的页式管理,查看了存储器从逻辑地址到线性地址到物理地址的变换,联合hello程序,fork和execve时的内存映射,了解了三级Cache支持下的物理内存访问的机制,以及缺页故障与缺页中断处置惩罚。
 
结论

用计算机体系的语言,逐条总结hello所经历的过程。
你对计算机体系的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法。
1. 源代码阶段
文件:hello.c
内容:程序员编写的高级C语言代码,包含main函数、printf等库函数调用。
作用:文本文件,由编辑器生成,是编译流程的输入源。

2. 预处置惩罚
内容:
睁开#include头文件(如stdio.h),插入原始代码。
替换宏定义(#define),删除注释。
添加行号标记,供编译器调试利用。
输出:hello.i——纯C代码,不再包含预处置惩罚指令。

3. 编译
内容:
词法/语法分析:将C代码转换为抽象语法树(AST)。
中间代码生成:生成与机器无关的中间表示(如LLVM IR)。
优化:删除冗余代码,简化算术操作。
目标代码生成:转换为x86-64汇编指令(AT&T格式)。
输出:hello.s——人类可读的汇编代码,含.text(代码)、.rodata(只读数据)等节。

4. 汇编
内容:
将汇编指令逐条翻译为机器码(二进制)。
分析符号(如printf),生成符号表(未定义符号标记为UND)。
记录需重定位的地址(如函数调用偏移量),生成重定位表。
输出:hello.o——可重定位目标文件(ELF格式),含.text、.data等段,但未绑定实际地址。

5. 链接
内容:
符号分析:将hello.o中的未定义符号(如printf)绑定到libc.so中的定义。
地址重定位:根据归并后的内存布局,修正代码中的偏移量(如.rodata字符串地址)。
段归并:将多个.o文件的.text、.data等段归并,分配终极虚拟地址。
动态链接处置惩罚:生成.plt(过程链接表)和.got(全局偏移表),支持运行时库加载。
输出:hello——可实行目标文件,含入口地址_start,可直接加载运行。

6. 进程创建
Shell举动:
分析命令./hello,调用fork()复制Shell进程(写时复制优化)。
子进程调用execve("./hello", argv, envp):销毁原内存空间,加载hello的ELF文件到新地址空间。映射.text(代码段)为只读,.data/.bss为可读写。动态链接器ld.so加载共享库(如libc.so),分析printf等函数地址。

7. 程序实行
CPU视角
从_start入口开始实行,调用__libc_start_main初始化运行时情况。
跳转到main函数:指令流:顺序实行printf、sleep等函数对应的机器码。内存访问:通过MMU将虚拟地址(VA)转换为物理地址(PA),TLB缓存加速转换。体系调用:printf触发write体系调用(陷入内核,int 0x80/syscall)。碰到exit()时,内核回收进程资源(内存、文件描述符等)。

8. 异常处置惩罚
典型场景:
缺页中断(Page Fault):访问未加载的虚拟页时,内核从磁盘换入数据。
信号(Signal):Ctrl+C发送SIGINT,Ctrl+Z发送SIGTSTP(挂起进程)。
内核相应:生存寄存器上下文,调用处置惩罚程序,规复实行或制止进程。

9. 资源回收
内容
父进程(Shell)调用wait()回收子进程状态。
内核释放进程页表、物理页、打开的文件等资源。
从进程列表中删除hello的PCB(进程控制块)。
结果:内存中不再有hello的陈迹,完成“020”(Zero to Zero)生命周期。
在整个过程中,计算机体系展现了精妙的协同工作机制:硬件层面的MMU和TLB加速地址转换,操作体系层面的页式管理实现虚拟内存,动态链接机制提高库函数复用率。这些设计不仅保证了程序实行的精确性,更在性能优化方面发挥了关键作用。通过此次研究,我们深刻体会到计算机体系设计中抽象分层的重要性,每一层都隐藏了下层的复杂性,同时为上层提供了简便的接口。未来可以在此基础上,进一步探索静态链接优化、更精致的进程监控等改进方向。
 

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

麻花痒

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表