计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 物联网工程
学 号 xxxxxxxxxx
班 级 xxxxxxx
学 生 xxx
指 导 教 师 吴锐
计算机科学与技术学院
2024年5月
摘 要
"Hello"程序的一生经历了多个阶段,从预处置处罚、编译、汇编、链接到进程管理、存储管理和I/O管理。在这些过程中,操纵系统、壳和硬件为其提供了必要的支持和保障,使其可以或许在计算机系统中完备地运行。本文详细探究了在Linux环境下,hello.c文件从编写到最终执行的整个生命周期。结合学习到的知识,逐步对比息争析各个过程在Linux中的实现机制及其缘故原由,并深入研究了hello.c文件的P2P和020的具体实现过程。
关键词:P2P;预处置处罚;编译;汇编;链接;进程管理;非常;
目 录
第1章 概述
1.1 Hello简介
1.2 环境与工具
1.3 中间效果
1.4 本章小结
第2章 预处置处罚
2.1 预处置处罚的概念与作用
2.2在Ubuntu下预处置处罚的命令
2.3 Hello的预处置处罚效果解析
2.4 本章小结
第3章 编译
3.1 编译的概念与作用
3.2 在Ubuntu下编译的命令
3.3 Hello的编译效果解析
3.4 本章小结
第4章 汇编
4.1 汇编的概念与作用
4.2 在Ubuntu下汇编的命令
4.3 可重定位目的elf格式
4.4 Hello.o的效果解析
4.5 本章小结
第5章 链接
5.1 链接的概念与作用
5.2 在Ubuntu下链接的命令
5.3 可执行目的文件hello的格式
5.4 hello的虚拟地点空间
5.5 链接的重定位过程分析
5.6 hello的执行流程
5.7 Hello的动态链接分析
5.8 本章小结
第6章 hello进程管理
6.1 进程的概念与作用
6.2 简述壳Shell-bash的作用与处置处罚流程
6.3 Hello的fork进程创建过程
6.4 Hello的execve过程
6.5 Hello的进程执行
6.6 hello的非常与信号处置处罚
6.7本章小结
第7章 hello的存储管理
7.1 hello的存储器地点空间
7.2 Intel逻辑地点到线性地点的变更-段式管理
7.3 Hello的线性地点到物理地点的变更-页式管理
7.4 TLB与四级页表支持下的VA到PA的变更
7.5 三级Cache支持下的物理内存访问
7.6 hello进程fork时的内存映射
7.7 hello进程execve时的内存映射
7.8 缺页故障与缺页中断处置处罚
7.9本章小结
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
8.2 简述Unix IO接口及其函数
8.3 printf的实现分析
8.4 getchar的实现分析
8.5本章小结
结论
附件
参考文献
第1章 概述
1.1 Hello简介
Hello.c经过预处置处罚器得到hello.i文件,然后经过编译得到hello.s,然后经过汇编得到hello.o,末了链接天生可执行文件hello(.out)。在shell中,fork产生一个子进程,就完成了从program到process。其编译执行的过程可分为P2P和020两个部分:
1.1.1 P2P(From Program to Process)
此过程指的是将C语言程序文件 hello.c 转换为可执行进程的过程。在 Linux 系统中,这个过程经历了如下几个步调:
- 预处置处罚(Preprocessing):使用C预处置处罚器cpp对hello.c进行预处置处罚,包罗宏睁开和头文件包含,天生hello.i。
- 编 译(Compiling):C编译器ccl(C Compiler)对hello.i进行词法分析、语法分析和优化等操纵,天生汇编代码hello.s。
- 汇 编(Assembling):汇编器as(Assembler)将hello.s翻译成呆板语言指令,天生一个目的文件hello.o。
- 链 接(Linking):链接器ld(Linker) 将hello.o和其他依赖的库文件进行链接,天生一个可执行文件hello。
- 运 行(Running):在shell内输入命令./hello,shell会通过系统调用fork()为其创建一个子进程,并在子进程当中执行hello。
1.1.2 020(From Zero-0 to Zero-0)
020是指将一个可执行文件hello.out载入内存并运行的过程。此过程中,0表示内存中没有hello文件的状态。具体而言,020在Linux下包含以下过程:
- 内存载入(Memory Loading):程序开始执行时,子进程调用execve函数将hello.out文件载入内存,并使用mmap函数将其映射到符合的内存位置。
- 进程控制(Process Control):内核中的进程控制器为hello进程分配时间片,使其开始执行自身的逻辑控制流。
- 进程回收(Process Reclamation):程序执行竣事后,父进程会回收hello进程,并在内核中删除相关数据,使内存规复到没有hello文件的初始状态。
1.2 环境与工具
- 硬件环境:X64 CPU;2.30GHz;16G RAM;1.5THD disk
- 软件环境:Windows11 64位;Vmware Workstation 17 Pro;Ubuntu 22.10
- 开发与调试工具:Visual Studio 2019 64位;CodeBlocks 64位;vim+gcc; readelf; objdump;ldd;EDB等
1.3 中间效果
文件名称
| 功能
| hello.i
| hello.c预处置处罚后天生的中间文件
| hello.s
| hello.i编译后天生的汇编文件
| hello.o
| hello.s汇编后天生的可重定位目的文件
| hello
| hello.o和其他库文件经链接后天生的可执行文件
| hello_o.elf
| 用readelf读取hello.o得到的ELF格式信息
| hello.elf
| 由hello可执行文件天生的ELF格式文件
| 表1 中间文件名称及功能
1.4 本章小结
以上过程详细描述了从C语言程序文件 hello.c 到可执行进程 hello 的转换过程以及相关的环境、工具和中间效果。通过预处置处罚、编译、汇编、链接和运行,将源代码转化为可在Linux系统中执行的进程。
(第1章0.5分)
第2章 预处置处罚
2.1 预处置处罚的概念与作用
预处置处罚是编译过程中的第一个阶段,它通过预处置处罚器对源代码进行处置处罚,天生一个经过预处置处罚的中间文件。
2.1.1 预处置处罚的概念
预处置处罚(Preprocessing)是计算机科学中编译器的一种紧张处置处罚阶段,用于在现实编译之前对源代码进行预处置处罚和转换,主要目的是为了简化编程工作,提高代码的复用性和可维护性。此过程并不包罗对源代码内容的解析,只是进行一些简朴的插入、删除和替换等文本操纵。
2.1.2 预处置处罚的作用
预处置处罚器(C Pre-Processor)的主要功能是在编译过程之前对源代码进行处置处罚,将源代码中的宏界说、条件编译指令、包含其他文件的指令等预处置处罚指令处置处罚完毕后天生新的源代码文件,以便编译器对其进行编译。预处置处罚器(preprocessor) 对程序源代码文本进行处置处罚,得到的效果再由编译器核心进一步编译。这个过程并不对程序的源代码进行解析,但它把源代码分割或处置处罚成为特定的单位——(用C/C++的术语来说是)预处置处罚暗号(preprocessing token)用来支持语言特性(如C/C++的宏调用)。
2.2在Ubuntu下预处置处罚的命令
预处置处罚的命令:gcc -E hello.c -o hello.i
图1 hello.c预处置处罚
2.3 Hello的预处置处罚效果解析
使用vim打开hello.i ,观察发现文件酿成了3601行,原来的main函数保存在文件末了,对于define预处置处罚,则检查程序中每一次出现的位置,做宏界说替换。而剩余部分则是对stdio.h、unistd.h、stdlib.h等头文件的包含睁开,同时删除了所有表明,使代码更加完备而不冗余。
图2 hello.i部分代码
2.4 本章小结
本章主要先容了预处置处罚的概念及作用、gcc下的预处置处罚指令,此外还对Hello的预处置处罚效果进行解析,明确了预处置处罚在整个P2P过程中的紧张性。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
得到hello.i后,我们需要对其进行编译,得到hello.s汇编文件。
3.1.1 编译的概念
编译(Compilation)是将高级编程语言(如C、C++等)的源代码转换成汇编语言(*.s)的过程。在编译的过程中,源代码经过词法分析、语法分析、语义分析等步调,转换成对应的中间表示情势,即汇编代码。编译的主要目的是提高程序的执行服从,使程序更加稳定和安全。
编译器是完成编译过程的程序,它将高级程序源代码作为输入,通过语法分析、语义分析、优化和代码天生等多个阶段,根据源代码的语法、数据类型、函数界说等信息,对源代码进行检查、转换和优化,天生对应的汇编代码文件。
3.1.2 编译的作用
- 代码转换:编译的主要作用是将源代码(用高级语言编写的程序)转换成目的代码(呆板语言或低级语言),从而使计算机可以或许明确和执行。
- 错误检查:编译过程会检查源代码中的语法错误,包罗拼写错误、语法结构错误等,并在编译时指出这些错误,以便开发者进行修正。
- 优化:编译器可以对源代码进行优化,以提高天生的目的代码的执行服从。优化可能包罗减少不必要的计算、消除冗余代码、重新组织指令次序等。
- 天生可执行文件:编译乐成后,会天生一个可执行文件,该文件包含了计算机可以执行的指令。用户可以直接运行这个可执行文件,而不需要每次执行时都重新编译源代码。
- 提高安全性:编译过程可以帮助提高软件的安全性。由于源代码是经过编译转化为目的代码的,因此可以防止未经授权的用户直接查看或修改源代码,从而在肯定水平上保护了软件的保密性和完备性。
3.2 在Ubuntu下编译的命令
编译的命令:gcc -S hello.i -o hello.s
图3 hello.i编译
3.3 Hello的编译效果解析
hello.i编译得到hello.s文件,以下将对hello.s使用的伪指令、编译过程中对各个数据类型的处置处罚以及各类操纵进行分析。
3.3.1 hello.s伪指令
内容
| 含义
| .file
| 源文件声明
| .text
| 代码段
| .section
| 指定接下来的指令和数据所属的段
| .rodata
| 只读代码段
| .align
| 指令大概数据的存放地点的对齐方式
| .global
| 声明全局符号
| .data
| 存放已经初始化的全局和静态变量
| .type
| 声明符号是数据/函数类型
| .long/.string
| 声明数据类型long/string
| 表2 hello.s伪指令及其含义
此处查看伪指令部分,.file指明文件名是hello.c,.text指示代码段,.section指示rodata段,.align8指明对齐方式。
图4 伪指令
3.3.2 hello.s数据
发生改变v去啊hello.s中包含常量和变量两种类型。
常量:字符串常量(存储在.rodata节)和整数常量(立即数情势)
图5、6 hello.s常量数据
变量:一般来说,过程通过减小%rsp的值为局部变量申请空间。汇编你代码中,%rsp被一次性减32,根据代码的上下文可知,从地点R[rrbp]-4到地点R[rrbp]的这段4Byte空间被用来存放int局部变量i。由此可知在本段汇编代码中,通过基于%rbp计算有效地点的方式实现对int类型的局部的引用。
int类型参数argc通过寄存器%edi传入main函数,之后movl将其拷贝到栈帧的局部变量区。代码中还有一些整型以立即数的情势出现,这些立即数纪录在代码区。
图7 汇编代码引用i的指令
3.3.3 hello.s数据操纵
hello.s中包含赋值、比较、加法以及数组操纵四种数据操纵。
- 比较:cmp指令(对argc&4、i&8的数值进行比较)
- 数组操纵:movq指令(输出时以及调用atoi函数时将argv[ ]内数据传出)
图8 对argv下标索引相关汇编指令的解析
3.3.4 hello.s控制转移
该代码涉及分支结构和for循环结构。C语言中的分支结构依靠if、else和switch等语句来实现。在本代码中,使用了if语句。在汇编代码层面,if语句通常通过cmp指令和条件跳转指令共同完成。
如图9所示,当执行cmpl指令时,假如-20(%rbp)的值即是4,ZF(零标志)会被设置为1,否则ZF会被重置为0。当执行je指令时,假如ZF的值为1,程序将跳转到.L2处的代码;假如ZF的值为0,程序将不会跳转,继承执行下一条指令。
图9 分支结构的汇编代码
C语言的for循环结构的实现也离不开跳转指令,也离不开关系操纵。如图 10所示,这个for循环起首由一个无条件跳转,在刚开始for循环时跳转到条件判定处.L3,然后假如满足条件,就跳转到.L4。当循环体的代码执行完之后,又次序执行到条件跳转。
图10 for循环结构的汇编代码
3.3.5 hello.s函数操纵
hello.s中包含参数传递、函数调用、函数返回等函数操纵。
- 控制传递:要执行过程Q,就要在开始调用Q后将程序计数器PC设置为Q的代码的起始地点;过程Q竣事之后,要把控制权转移给过程P,于是在返回后,要把程序计数器设置为P中调用Q的指令的下一条指令的地点。
- 传递数据:P要可以或许向Q提供0个、一个或多个参数,Q通常会给P返回一个值(通常通过%rax/%eax/%ax%al寄存器返回)。
- 分配和开释内存:在开始时,Q可能需要为局部变量分配空间,而在返回前,又必须开释这些空间。
程序中涉及的函数操纵枚举如下(x86-64系统):
· 控制传递:系统启动函数__libc_start_main使用call指令调用main函数。call指令将下一条指令的地点压入栈中,然后将%rip寄存器的值设置为main函数的起始地点。
· 数据传递:__libc_start_main向main函数传递参数argc和argv,分别存储在%edi(argc,类型为int)和%rsi(argv)寄存器中。main函数的return 0对应汇编中的三条指令:将%eax设置为0,然后执行ret(中间的leave指令稍后分析)。ret指令从栈中弹出返回地点并赋值给%rip。
· 内存分配与开释:%rbp寄存器纪录对应栈帧的最高地点减8的值。通过减小%rsp的值在栈中分配空间,程序竣事时调用leave指令。leave指令将%rbp的值赋给%rsp(开释局部变量占用的空间),然后从栈中弹出一个4字节长的值给%rbp(现实上是__libc_start_main函数的%rbp值),规复栈空间到调用main函数之前的状态。
· 数据传递:第一次调用printf时,将%rdi寄存器设置为字符串"用法: Hello 学号 姓名 电话号码 秒数!\n"的首地点。第二次调用printf时,%rdi寄存器设置为字符串"Hello %s %s\n"的首地点,%rsi寄存器设置为argv[1],%rdx寄存器设置为argv[2]。
· 控制传递:第一次调用printf只有一个字符串参数,以是使用call puts@PLT;第二次调用printf使用call printf@PLT。
传递数据:将%edi设置为1。
控制传递:call exit@PLT。
传递数据:将%rdi设置为argv[3]。
控制传递:call atoi@PLT。
传递数据:将%edi设置为&eax(即atoi函数返回的值)。
控制传递:call sleep@PLT。
控制传递:call gethcar@PLT
3.4 本章小结
本章先容了编译的概念和作用,并着重分析了编译天生的汇编代码。
汇编代码是低级语言,机械难懂,但是它是很多高级语言的底子。它更靠近于CPU,这使得它的代码的可移植性差,但也使得它可以提供更多操纵CPU的方法(C语言只是使用了CPU的指令集的一个子集)。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
汇编文件hello.s已经具备在指令级别上控制CPU进行数据传递等操纵,但仍需要将汇编语言汇编天生呆板语言hello.o来实现程序的执行。
4.1.1 汇编的概念
汇编是将编译后的汇编语言程序(.s)翻译成呆板可识别和执行的呆板指令,并将这些指令打包成一种叫做可重定位目的程序,进而转换成呆板语言程序(.o)的过程。它是编译器天生可执行文件的一个紧张步调。
汇编程序使用一些特殊的指令,这些指令直接对应底层的硬件架构,因此天生的呆板语言程序可以直接在计算机上运行。
4.1.2 汇编的作用
汇编可以或许将人类可读的汇编代码转化为呆板可执行的指令,这些指令可以被计算机处置处罚器直接识别和执行。通过汇编,程序员可以直接控制计算机底层的操纵,实现高效的程序代码。
因此,汇编在操纵系统、嵌入式系统、驱动程序等领域有着广泛的应用。同时,汇编也是高级语言编译器、表明器等软件工具的紧张底子,因为这些工具需要将高级语言翻译成汇编代码后再进行处置处罚。
注:这里的汇编是指从.s到.o即编译后的文件到天生呆板语言二进制程序的过程。
4.2 在Ubuntu下汇编的命令
汇编的指令:gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o
图11 汇编命令
4.3 可重定位目的elf格式
起首输入命令readelf -a hello.o > hello_o.elf,得到hello.o的ELF格式。观察发现hello.elf文件包含ELF头(ELF Header)、节头(Section Header)、符号表、重定位节等部分。
4.3.1 ELF头
ELF头部从一个16字节的magic序列开始,描述了天生文件的系统的字大小和字节次序,前4字节7f 45 4c 46分别对应删除(Del) E L F的ASCII码。操纵系统在加载可执行文件时会验证magic序列的正确性。 Header的别的部分包含帮助链接器进行语法分析和表明目的文件的信息,包罗数据类型和字节次序、操纵系统/ABI、目的文件类型(如REL可重定位、EXEC可执行或共享的)、呆板类型(如x86-64)、节头部表的文件偏移量、规模以及条目的大小和数量等相关信息。
4.3.2 节头
节头列表包含文件中各节的名称、类型、地点、偏移量、大小、旗标、链接和对齐信息等内容,在hello.elf文件中共包含13个节。
4.3.3 符号表
符号表存放程序中界说和引用的函数和全局变量的相关信息,包罗相对于目的节的起始位置偏移Value、大小Size、类型Type/Bind以及符号名称Name。由于在此还没有进行相关库函数的链接,因而Value都为0。
4.3.4 重定位节
此部分包含main.o中需要重定位的信息,当链接器把目的文件和其他文件组合时,需要根据具体类型修改这些位置。每条重定位条目包含偏移量、信息、类型(标识对应的地点计算算法)、符号值、符号名称、加数等信息。下图.rela.text是hello.c中调用函数的重定位条目;.rela.eh_frame是对.text代码段的重定位条目。
4.4 Hello.o的效果解析
通过objdump -d -r hello.o反汇编hello.o,并与hello.s对照如下图所示。发现反汇编代码与hello.s基本相似,汇编后调用函数以及访问内存时还需链接器作用才能确定命据/函数地点,而hello.s与反汇编代码在处置处罚这部分存在一些区别:
- 分支转移:在反汇编文件中,跳转目的使用的是PC相对的地点,即目的指令地点与当前指令下一条指令的地点之差的补码表示。而在.s文件中,通常使用段名称进行跳转。
- 数制表示:反汇编中通常使用十六进制数,而.s文件中使用的是十进制数。
- 函数调用:在反汇编中,call的目的地点是当前下一条指令的地点。而在.s文件中,函数调用之后直接跟着函数名称。
图12 hello.o反汇编与hello.s部分对照
4.5 本章小结
本章先容汇编基本概念,通过分析hello.o文件和elf文件,研究了elf文件的结构。在比较反汇编文件和汇编文件的细节中,发现二者的一些区别。
(第4章1分)
第5章 链接
5.1 链接的概念与作用
链接是将各种代码和数据片段网络并组合成一个单一文件的过程,这个文件可以被加载到内存中执行。链接可以在编译时、加载时或运行时进行。链接的主要作用包罗:
- 组织程序:通过将各种代码和数据片段链接在一起,形成一个单一文件,便于程序的组织和管理。
- 提高服从:链接过程可以消除重复的代码和数据,减少内存占用,提高程序的执行服从。
- 实现模块化开发:通过链接,可以将程序分解为多个模块,每个模块可以独立开发、编译和测试,从而实现模块化开发,提高开发服从。
- 处置处罚依赖关系:链接过程可以处置处罚差别模块之间的依赖关系,确保程序在编译和运行时正确链接到所需的模块和库。
- 实现动态加载:通过动态链接,可以在程序运行时加载差别的模块或库,实现动态加载和卸载功能。
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
5.3 可执行目的文件hello的格式
输入指令readelf -a hello > hello.elf天生hello程序的ELF格式文件,观察各段的基本信息,并与上文4.3节中由hello.o天生的hello_o.elf进行对比。
5.3.1 ELF头
ELF头与hello.elf的基本雷同,差别之处在于文件类型由REG变为EXEC可执行目的文件,同时节头大小数量增长,并得到了入口地点。
图13 ELF头信息
5.3.2 节头
链接后,节头数量有所增长,但各条目内包含信息种类并未发生变化。
图14 ELF节头信息
5.3.3 符号表
链接后,符号表条目显着增长,包含各目的文件中符号界说及引用信息。
图15 ELF符号表信息
5.3.4 重定位节
链接后,重定位节内容变化为执行过程中需要通过动态链接调用的函数,同时类型也发生改变。
图16 ELF重定位节信息
5.3.5 其他内容
链接后,相较于hello_o.elf,hello.elf增长了程序头和段节。二者均是在链接过程中确定,其中程序头描述了系统预备程序执行所需的段和其他信息。
图17 hello.elf新增部分
5.4 hello的虚拟地点空间
使用edb加载hello,观察Data Dump部分,观察发现程序占有0x401000~ 0x402000的地点空间,通过5.3节中入口地点及各节的偏移量即可观察各节内容。
图18 edb虚拟内存窗口
5.5 链接的重定位过程分析
输入指令objdump -d -r hello,观察其中的的main函数,并与4.4节中hello.o的反汇编代码进行对比。
图19 反汇编对比(左为hello反汇编,右为hello.o反汇编)
起首,Hello.o.objdump文件中只有main函数,而Hello.objdump文件中不仅有main函数,还多了其他函数。这一变化与链接过程密切相关。
在使用ld命令进行链接时,指定了动态链接器为64位的/lib64/ld-linux-x86-64.so.2。crt1.o、crti.o、crtn.o主要界说了程序入口_start和初始化函数_init,_start程序调用Hello.c中的main函数。libc.so是动态库,其中界说了Hello.c中用到的printf、sleep、getchar、exit函数,以及_start中调用的__libc_csu_init、__libc_csu_fini、__libc_start_main。链接器将这些函数加入到目的文件中,使得Hello程序中多了好几个函数。这些函数的加入发生在符号解析过程中。
其次,Hello程序中的各个字节在运行时都有了虚拟地点,而Hello.o文件中只有节偏移信息。在符号解析之后,链接器对输入的目的模块的代码节和数据节进行重定位。使用objdump -d -r hello可以分析hello与hello.o的差别,阐明链接过程。
结合hello.o的重定位条目,分析Hello程序中的重定位过程。静态库符号的重定位由链接器完成,动态库符号的重定位由动态链接器完成。这里先分析静态链接中的重定位过程。
以第一个常量字符串的重定位为例。通过重定位条目,链接器知道这个重定位的类型。固然难以得到符号解析后重定位之前的重定位条目,但通过使用gdb、objdump和edb等工具,可以反推出第一个常量字符串的重定位条目,设为r。
- r.offset = 0x51
- r.addend = -4
- ADDR(.text) = 0x4010f0
- ADDR(r.symbol) = 0x402008(第一个常量字符串的首地点,并不是.rodata的首地点)
addr(.text)表示.text节在文件中的首地点。编译器会计算文件中要修改的首地点refptr = addr(.text) + r.offset;再计算运行时PC相对地点,然后将这个地点写入以refptr为首地点的一连四个字节:*refptr = ADDR(r.symbol) + r.addend – (ADDR(.text) + r.offset) = 0x402008 + (-0x4) - 0x401141 = 0xec3。观察发现确实是0xec3。
5.6 hello的执行流程
使用gdb/edb执行hello,阐明从加载hello到_start,到call main,以及程序终止的所有过程(主要函数)。请列出其调用与跳转的各个子程序名或程序地点。
程序名称
| 程序地点
| _start
| 0x00000000004010f0
| __lib_start_main(于libc.so.6)
| 0x00007fbed3e29dc0
| __cxa_atexit(于libc.so.6)
| 0x00007fbed3e458c0
| _init
| 0x0000000000401000
| main
| 0x0000000000401125
| puts@plt
| 0x401090
| exit@plt
| 0x4010d0
| getchar@plt
| 0x4010b0
| printf@plt
| 0x4010a0
| atoi@plt
| 0x4010c0
| sleep@plt
| 0x4010d0
| exit (于libc.so.6)
| 0x00007ff9a7c455f0
| 表2 hello执行流程
5.7 Hello的动态链接分析
通过使用edb/gdb进行调试,可以分析hello程序在动态链接过程中的变化。在动态链接之前和之后,可以观察到程序中各个项目的内容变化。动态链接器在函数重定位时使用延迟绑定策略,将函数地点的绑定推迟到第一次调用该函数时才进行。以printf函数为例,分析其在动态链接初始化(dl_init)前后的GOT和PLT内容变化。在个人的Ubuntu环境中,PLT表的结构与教科书中有所差别,如图20所示。图中绿箭头指向的指令跳转到动态链接器,动态链接器根据栈中的参数修改GOT中相应条目的地点。这样,下一次跳转到蓝色高亮指令时,就会直接跳转到printf函数的现实地点。
图20 edb查看PLT
图21调用前.got
调用后,.got中的条目已经改变,阐明动态链接完成。
图22 调用后.got
5.8 本章小结
本章讲述了链接的概念和多用,重点对静态链接和动态链接进行了分析,还理清了程序的加载过程。
(第5章1分)
第6章 hello进程管理
6.1 进程的概念与作用
6.1.1 进程的概念
一个进程是一个执行中程序的实例。
6.1.2 进程的作用
进程简化了用户的内存操纵的工作,提高了程序的通用性,是多个过程并发执行的底子,是计算机科学中最深刻,最乐成的概念。
6.2 简述壳Shell-bash的作用与处置处罚流程
壳(Shell)是一种命令表明器,是用户与操纵系统之间的接口。如Windows下的命令行表明器,cmd、powershell,图形界面的资源管理器。Linux下的Terminal/tcsh、bash等等,也包罗图形化的GNOME桌面环境。Shell是信号处置处罚的代表,负责各进程创建与程序加载运行及前背景控制,作业调用,信号发送与管理等。Shell是人在操纵系统中的代表。
Bash的处置处罚流程大致可以分为以下几个步调:
· 读取用户输入:读取用户输入的命令或脚本文件。
· 解析输入:将输入字符串切分,分析输入内容,解析命令和参数,并将命令行的参数转换为系统调用execve()所要求的情势。
· 判定命令类型:判定命令是否为内置命令。假如是,则立即执行;否则,调用fork()来创建子进程,自身调用wait()来等待子进程完成。在此期间,程序始终担当键盘输入信号,并对输入信号进行相应处置处罚。
· 执行命令:子进程运行时,调用execve()函数,根据命令名查找可执行文件,将其加载到内存中并执行。
· 处置处罚完成:子进程完成处置处罚后,向父进程(即shell)报告。此时,终端进程被唤醒,完成必要的判别工作后,表现提示符,等待用户输入新命令。
6.3 Hello的fork进程创建过程
在命令行输入./hello命令运行hello程序时,由于该命令不是内置命令,Bash会通过调用fork函数创建一个新的子进程。fork函数执行过程中,操纵系统会为新的子进程分配一个新的标识符(PID),然后在内核中分配一个进程控制块(PCB),并将其挂在PCB表上。接着,操纵系统会将父进程的环境复制到子进程中,包罗大部分PCB的内容,并为其分配资源,包罗程序、数据、栈等。
新创建的子进程几乎与父进程雷同,但并不完全雷同。子进程会得到与父进程用户及虚拟地点空间雷同的(但独立的)一份副本,包罗代码段、数据段、堆、共享库以及用户栈。子进程还会得到与父进程任何打开文件描述符雷同的副本,这意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大的区别在于它们有差别的PID。
6.4 Hello的execve过程
执行execve系统调用时,操纵系统会起首清空当前子进程的用户空间栈,然后将待执行程序的命令行参数(argv)和环境变量(envp)压入栈中。随后,控制权转移到hello程序的入口点,即main函数。在这个过程中,execve还会将hello程序所需的库文件加载到内存中,并初始化程序所需的内存空间。假如execve乐成执行,当进步程的代码和数据将被新程序内容完全替换,新的程序作为新的进程映像开始运行。假如执行过程中出现错误,好比找不到指定程序,execve会返回负值,表示执行失败,此时原有的进程将继承运行。
6.5 Hello的进程执行
hello进程在操纵系统中的执行是由进程调度器对进程进行时间片调度,并通过上下文切换实现进程的执行。在执行过程中,操纵系统合理调度,根据需要在用户态和核心态之间进行切换,并在进程竣事后清除其资源。
6.5.1 进程调度的概念
进程调度是操纵系统管理进程并分配处置处罚器资源的过程。在进程执行期间,处置处罚器会按照肯定的时间片轮流使用各个进程。
在进程执行过程中,操纵系统可以随时决定抢占当前正在执行的进程,并开始执行另一个进程。这种决策通常基于进程的优先级、等待时间、资源使用环境等因素。被抢占的进程的上下文信息,如寄存器状态、程序计数器、用户栈和内核栈等,会被生存到内核中,以便在需要时可以或许规复该进程的原始状态。
内核通过上下文切换机制来实现进程的抢占和切换。在上下文切换过程中,当进步程的上下文信息会被生存到内核中,然后加载下一个进程的上下文信息,并将控制权转移到新进程的主函数中。这个过程确保了多个进程可以或许在处置处罚器上有效地共享时间,从而实现并发执行。
6.5.2 用户态与核心态
操纵系统中存在两种特权级别:用户模式和内核模式。用户模式下,进程只能访问自己的地点空间,不允许直接访问内核区的代码和数据;而内核模式下,进程可以执行指令集中的任何命令,并访问系统中的任何内存位置。这样的划分包管了系统的安全性,防止用户程序直接访问内核数据结构或系统硬件资源
当操纵系统决定运行hello进程时,将在进程调度器中生存当前执行进程的上下文信息,并将控制权转移到hello进程的上下文。此时,CPU进入用户态。
当hello进程需要执行需要特权级别的操纵(如I/O操纵),会导致CPU进入核心态,此时操纵系统会生存当进步程的上下文,并执行需要的特权操纵。完成后,操纵系统将控制权返回给hello进程,CPU重新进入用户态,并将生存的上下文信息规复到CPU中。
图23 进程上下文切换
6.6 hello的非常与信号处置处罚
hello程序执行过程中出现的非常可能有中断、陷阱、故障、终止等
类别
| 缘故原由
| 异步/同步
| 返回行为
| 中断
| 来自I/O设备的信号
| 异步
| 总是返回到下一条指令
| 陷阱
| 故意的非常
| 同步
| 总是返回到下一条指令
| 故障
| 潜伏可规复的错误
| 同步
| 可能返回到当前指令
| 终止
| 不可规复的错误
| 同步
| 不会返回
| hello具体运行过程中,可能产生SIGINT、SIGKILL、SIGSEGV、SIALARM、SIGCHLD等信号,具体处置处罚如下:
- Ctrl+Z:进程收到SIGSTP信号,hello停止,此时进程并未回收,而是背景运行,通过ps指令可以对其进行查看,还可以通过fg指令将其调回前台。
图24 SIGSTP信号处置处罚
- Ctrl+C:进程收到SIGINT信号,hello终止。在ps中查询不到此进程及其PID,在jobs中也没有表现。
图25 SIGINT信号处置处罚
图26 键盘乱按处置处罚
- kill命令:挂起的进程被终止,在ps中无法查到到其PID。
图27 kill指令
图28 pstree指令
6.7本章小结
简要先容进程的基本概念,先容fork函数和execve函数。先容信号处置处罚和非常处置处罚的基本知识,在程序上对多种键盘输入进行测试。
(第6章1分)
第7章 hello的存储管理
7.1 hello的存储器地点空间
逻辑地点:这是程序代码经过编译后出如今汇编程序中的地点。逻辑地点是用来指定一个操纵数大概是一条指令的地点,通常是指相对于某个基准点的偏移量。在有地点变更功能的计算机中,访问指令给出的地点(操纵数)就是逻辑地点,也称为相对地点。逻辑地点需要经过寻址方式的计算或变更才能得到内存储器中的物理地点。
线性地点:也称为虚拟地点,它是一个无符号整数,通常用来表示高达4GB的地点空间,也就是高达4294967296个内存单位。线性地点通常用十六进制数字表示,值域从0x00000000到0xfffffff。线性地点经过段机制的转换后成为物理地点。
物理地点:这是CPU地点总线传来的地点,由硬件电路控制。物理地点用于内存芯片级的单位寻址,与处置处罚器和CPU毗连的地点总线相应。
在这个存储器地点空间中,逻辑地点和线性地点都只是中间层的抽象,而物理地点是最终被硬件所识别的地点。这些地点空间的概念是操纵系统进行内存管理和进程调度的底子。
执行hello程序时,程序的指令和数据起首被加载到逻辑地点空间,经过分段、分页转换后得到线性地点,最终通过页表机制转换为物理地点,才能被现实执行。
7.2 Intel逻辑地点到线性地点的变更-段式管理
Intel处置处罚器中,段式存储管理中以段为单位分配内存,每段分配一个一连的内存区,但各段之间不要求一连,且长度不一。其优点是易于编译、管理、修改和维护,但会产生内存的浪费。
完备的逻辑地点包含段选择符和段内偏移地点两部分。段选择符选择对应的段,在x86保护模式下,段描述符(段基线性地点、长度、权限等)无法直接存放在段寄存器中。Intel处置处罚器将段描述符集中存放在GDT或LDT中,而段寄存器存放的是段描述符在GDT或LDT内的索引值。
将段首地点作为基地点加上偏移地点即可将逻辑地点映射到线性地点空间。
7.3 Hello的线性地点到物理地点的变更-页式管理
Hello程序的线性地点到物理地点的变更是通过页式管理来实现的,这是一种高级的内存管理技术。页式管理将物理内存划分为固定大小的页框(page frame),并将逻辑内存也划分为雷同大小的页面(page)。每个页面可以映射到任何一个空闲的页框中。
具体的线性地点到物理地点的变更过程如下:
- CR3寄存器:处置处罚器从CR3控制寄存器中获取页目录的基地点。CR3寄存器中存储的是页目录的起始物理地点。
- 页目录:线性地点的高10位被用作索引来访问页目录。页目录项中存储了对应页表的基地点。
- 页表:处置处罚器使用线性地点的中间10位作为索引来访问页表。页表项中存储了目的页面的物理地点大概页面属性等信息。
- 物理地点:末了,处置处罚器将线性地点的低12位作为页内偏移,与页表项中的物理地点相加,得到最终的物理地点。
通过页式管理,操纵系统可以更加灵活地进行内存分配和管理。程序使用的逻辑内存可以被映射到不一连的物理内存上,提高了内存的使用率。同时,页式管理还提供了内存保护机制,每个页面都有自己的访问权限和属性,可以防止程序越界访问大概非法修改其他程序的内存空间。
7.4 TLB与四级页表支持下的VA到PA的变更
TLB(Translation Lookaside Buffer)是一种高速缓存,用于存储近来被使用的虚拟地点(VA)到物理地点(PA)的映射关系。当CPU访问一个虚拟地点时,起首会查询TLB。假如在TLB中找到了对应的映射(命中),CPU就可以直接获取物理地点;假如未找到(未命中),则需要通过页表进行地点翻译。
在四级页表的支持下,CPU访问一个虚拟地点时,会提取出该虚拟地点中的页目录项索引、页表项索引和页内偏移,并通过四级页表来查找物理地点。四级页表中的每一级都有对应的页目录表和页表,可以将虚拟地点转换为物理地点。假如TLB未命中,会触发一次页表缺失非常,内核会处置处罚该非常并将相关条目添加到TLB中。
TLB和四级页表结合使用可以或许显着提高地点翻译的服从,减少对内存访问的次数,从而提拔系统的整体性能。
图29 Inter Core i7地点翻译
7.5 三级Cache支持下的物理内存访问
三级缓存是一种采用多级缓存的存储体系,可以提高计算机内存访问速度和服从。在三级缓存的架构中,缓存分为L1、L2和L3三级,每一级缓存都有差别的容量和访问速度。
当CPU需要访问内存时,起首在L1缓存中查找数据,先找组索引位,然后与标志位对比。假如L1缓存中未命中,则需要从存储层次结构中的下一层(即L2缓存)查找。如若仍未命中,则会继承在L3缓存中查找。假如在L3缓存中也未命中,则会从主存中获取数据。
图30 Inter Core i7的Cache结构
假如在三级缓存中找到了需要的数据,则可以直接访问缓存中的数据,从而提高访问速度。假如在三级缓存中没有找到,则需要从主存中获取数据,并将数据存入三级缓存中,以便下次访问时可以更快地获取数据。
7.6 hello进程fork时的内存映射
当一个进程使用fork()系统调用创建一个新的进程时,新进程(子进程)会继承父进程的内存映射。这意味着子进程将得到父进程的代码段、数据段、堆和栈等内存地区的副本。
以下是fork()系统调用后父子进程的内存映射环境:
- 代码段:子进程复制父进程的代码段,这是必须的,因为子进程需要执行雷同的程序代码。
- 数据段:子进程复制父进程的数据段,包罗全局变量和静态变量等。
- 堆:子进程通常会从堆的起始位置开始复制父进程的堆地区。但要留意,子进程可能不会复制整个堆,而是使用适当的内存管理机制来分配和开释内存。
- 栈:子进程也会复制父进程的栈地区。这是为了生存函数调用的返回地点和局部变量等。
需要留意的是,固然子进程继承了父进程的内存映射,但它们是独立的进程,有自己的虚拟地点空间。对子进程的内存进行修改不会影响父进程的内存,反之亦然。此外,父子进程可以使用exec()系列函数来替换各自的内存映像,执行差别的程序。
总之,fork()系统调用创建了一个与父进程几乎完全雷同的子进程,包罗内存映射。这使得操纵系统可以或许快速地创建新进程,并为其提供必要的资源来执行任务。
7.7 hello进程execve时的内存映射
execve函数在当进步程中加载并运行新程序hello时,需要以下几个步调:
- 删除已有页表和结构体vm_area_struct链表,删除当进步程虚拟地点的用户部分中的已存在的地区结构;
- 创建新的页表和结构体vm_area_struct链表,包罗目的文件提供的代码和初始化的数据映射到.text和.data段,.bss和栈映射到匿名文件。所有这些新的地区都是私有的写时复制的。
- 将需要动态链接的libc.so映射到用户虚拟地点空间中的共享地区内。
- 设置程序计数器(PC),指向代码地区的入口点,Linux根据需要换入代码和数据页面。
7.8 缺页故障与缺页中断处置处罚
缺页故障指访问一个虚拟地点的数据时,对应的物理地点不在内存中,从而引发的非常环境。页面命中完全是由硬件完成的,而处置处罚缺页非常是由硬件和操纵系统内核协作完成的。
当发生缺页故障时,处置处罚器会向操纵系统发出缺页中断信号,缺页处置处罚程序确定物理内存中的牺牲页(若页面被修改,则换出到磁盘),而后调入新的页面,并更新内存中的PTE。末了缺页处置处罚程序返回到原来进程,再次执行导致缺页的指令,从而完成内存访问。
7.9本章小结
本章简要先容存储相关的知识。先容差别地点概念以及他们之间的转换,讲述fork和execve函数的存储映射,末了先容缺页处置处罚.
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
Linux的IO设备管理方法主要包罗以下几种:
- 在Linux中,所有的设备都被视为文件。这意味着,对设备的访问可以通过文件系统接口(如read、write、open、close等系统调用)来完成。每个设备被映射到文件系统的某个位置,称为设备文件。
- 设备文件通常位于/dev目录下,以文件名的情势存在,如/dev/sda代表磁盘设备、/dev/tty1代表终端设备等。
- 设备驱动程序是Linux内核的一部分,负责与硬件设备进行通信和控制。
- 设备驱动程序向上提供同一的接口,供文件系统调用,使得应用程序可以通过标准文件IO操纵读写设备。
- 设备驱动程序向下直接操纵硬件设备,使用特定于设备的协媾和控制方式进行通信。
- 字符设备:以字符为单位进行访问的设备,如终端设备、串口设备等。
- 块设备:以块为单位进行访问的设备,如硬盘、闪存等。
- Linux中的设备文件具有权限属性,雷同于普通文件,可以通过chmod和chown命令来修改权限。
- 访问设备通常需要root大概有相应权限的用户。
- 设备驱动程序可以使用中断机制大概轮询方式来处置处罚设备的输入输出哀求。
- 中断:当设备有数据预备好时,设备驱动程序会触发中断,通知内核进行数据处置处罚。
- 轮询:设备驱动程序定期查询设备状态,检查是否有数据需要处置处罚。
- Linux内核通过设备树(Device Tree)来管理系统中的硬件设备。设备树是描述硬件架构和毗连信息的数据结构,用于在运行时动态构建立备的层次结构。
- 设备树使得内核可以或许动态识别和管理各种设备,包罗处置处罚器、外围设备等。
8.2 简述Unix IO接口及其函数
(以下格式自行编排,编辑时删除)Unix IO接口是通过文件描述符来访问各种IO设备的同一方式,它提供了一组函数来进行文件和设备的读写操纵。以下是Unix IO接口中常见的函数及其作用:
int open(const char *path, int flags, mode_t mode)
打开指定路径的文件,并返回文件描述符。
path:文件路径。
flags:打开文件的方式,如只读、只写、读写等。
mode:文件权限,仅在创建文件时有效。
int close(int fd)
关闭指定文件描述符的文件。
ssize_t read(int fd, void *buf, size_t count)
从文件描述符 fd 指定的文件中读取 count 字节数据到 buf 中。
ssize_t write(int fd, const void *buf, size_t count)
将 buf 中 count 字节的数据写入到文件描述符 fd 指定的文件中。
off_t lseek(int fd, off_t offset, int whence)
设置文件描述符 fd 指定的文件的读写位置。
offset:偏移量。
whence:起始位置,可以是 SEEK_SET(文件开头)、SEEK_CUR(当前位置)、SEEK_END(文件末端)。
int dup(int oldfd)
复制文件描述符 oldfd,返回新的文件描述符。
int dup2(int oldfd, int newfd)
将文件描述符 oldfd 复制到 newfd,假如 newfd 已经打开,则先关闭。
int fstat(int fd, struct stat *buf)
获取文件描述符 fd 关联的文件信息,并将其存储在 buf 结构体中。
int ioctl(int fd, unsigned long request, ...)
提供对特定设备的控制操纵。
int perror(const char *s)
打印近来的系统错误信息,以字符串 s 开头。
8.3 printf的实现分析
printf的实现分析可以从用户程序调用开始,一直到字符表如今屏幕上的整个过程,包罗库函数、系统调用、表现驱动等的协同工作。
printf("Hello, world!\n");
- 库函数vsprintf: printf函数起首调用vsprintf函数,将格式化后的字符串写入到缓冲区中。vsprintf函数的主要工作是将格式化的数据写入到内存中的缓冲区。这一步涉及到字符串的处置处罚和格式化,比方将"Hello, world!\n"转换为ASCII码。
- write系统调用: 当缓冲区中的内容预备好后,printf通过write系统调用将数据发送到标准输出,即终端或控制台。write系统调用会将数据从用户空间复制到内核空间的缓冲区,并由内核进一步处置处罚。
- 内核中的处置处罚:
系统调用处置处罚:内核收到write系统调用后,会执行系统调用处置处罚程序。
调度到具体设备:内核会将数据调度到具体的输出设备,比方表现器。
字符表现驱动子程序:内核调用相应的字符表现驱动子程序,它负责将数据从内核缓冲区传输到物理表现设备上的VRAM(视频内存)。
从ASCII到字模库:字符表现驱动子程序将ASCII字符转换为相应的字模(字形)。
表现VRAM:将经过处置处罚的字模数据写入到VRAM中,VRAM中存储了每个点的RGB颜色信息。
刷新频率:表现芯片按照设定的刷新频率逐行读取VRAM中的数据。
信号传输:根据VRAM中存储的RGB颜色信息,表现芯片通过信号线将每个像素的RGB分量传输到液晶表现器。
液晶表现器:液晶表现器接收到RGB信号后,将每个像素的颜色表如今屏幕上。
8.4 getchar的实现分析
- 键盘中断处置处罚子程序:当用户按下键盘时,会触发键盘中断。处置处罚器会跳转到预先界说的中断处置处罚程序(IRQ1),即键盘中断处置处罚子程序。
- 按键扫描码转换:键盘中断处置处罚程序会读取键盘控制器中的扫描码(scan code),并将其转换为ASCII码或其他字符编码。
- 生存到键盘缓冲区:转换后的字符或ASCII码会被生存到系统的键盘缓冲区中,等待进程调用read系统函数读取。
· 用户空间调用:在用户程序中调用getchar函数,比方:
char c = getchar();
调用过程:getchar函数现实上会调用read系统调用来获取用户输入的字符。
- 调用read:getchar函数内部会调用read系统调用,以读取标准输入(键盘)上的字符。
- 系统调用执行:read系统调用会将字符从内核空间的键盘缓冲区复制到用户空间的缓冲区中。
- 循环读取:read系统调用在内核中会循环等待键盘输入。
- 直到回车键:系统调用会一直等待,直到用户按下回车键,将回车键的ASCII码('\n')返回给用户程序。
返回字符:一旦read系统调用收到回车键,getchar函数将返回这个字符给用户程序。
8.5本章小结
本章先容了Linux的IO设备管理方法和Unix IO接口及其函数,而且详细叙述了printf函数和getchar函数的执行流程,包罗系统调用和IO管理。
(第8章1分)
结论
经过对hello.c程序从创建到执行的全过程探索,我对程序的生命周期有了更深刻的认识。
起首,hello.c文件经过预处置处罚,天生了hello.i文件。接着,hello.i文件被编译,天生了hello.s汇编文件。随后,hello.s文件经过汇编,天生了可重定位目的文件hello.o。最终,hello.o文件与所需的库函数经过动态链接,天生了可执行目的文件hello。
运行hello时,起首由bash shell调用fork函数天生子进程,再调用execve函数载入hello程序,为其分配虚拟内存。当执行到入口点时,程序被载入物理内存。hello程序在CPU中独占一段时间,执行自己的逻辑控制流。在此期间,CPU通过MMU使用TLB和页表将虚拟地点映射为物理地点,进行内存访问。当程序遇到信号时,它会调用信号处置处罚程序进行处置处罚。最终,程序执行完毕后,shell父进程回收fork出来的子进程,内核删除该过程中创建的统统数据结构。
通过对hello程序生命周期的研究,我对程序的动态链接、shell的操纵、fork和execve函数的作用、虚拟内存的功能、堆管理、IO管理以及信号处置处罚等有了更深的明确。最初,我对虚拟内存和信号管理感到狐疑,但随着一步步的探索,终于明确了其中的原理。随着对《计算机系统底子》课程的逐步明确,hello程序也走完了它的一生。
此次学习让我认识到计算机系统是由硬件和软件组成的复杂交互系统。硬件包罗中央处置处罚器、存储器和输入输出设备等物理组件;软件是运行在计算机上的程序和数据的聚集。硬件和软件的协同工作,使计算机可以或许实现各种复杂功能。
在学习过程中,我了解到计算机的基本工作原理。当用户在键盘上输入一个字符时,键盘接口将字符发送到中央处置处罚器,中央处置处罚器根据存储器中的程序指令处置处罚字符,并将效果存储回存储器或输出到表现器。这一过程中涉及数据在差别部件之间的传输和转换,包罗数据总线和地点总线。
此外,我对操纵系统有了更深入的了解。操纵系统是计算机系统的核心软件,负责管理硬件和软件资源,提供用户界面和应用程序接口。通过学习操纵系统的基本原理,我明确了进程管理、内存管理、文件系统和设备驱动程序等紧张概念。
(结论0分,缺失 -1分,根据内容酌情加分)
附件
列出所有的中间产物的文件名,并予以阐明起作用。
文件名称
| 功能
| hello.c
| hello的C语言源代码
| hello.i
| hello.c预处置处罚后天生的中间文件
| hello.s
| hello.i编译后天生的汇编文件
| hello.o
| hello.s汇编后天生的可重定位目的文件
| hello
| hello.o和其他库文件经链接后天生的可执行文件
| hello_o.elf
| 用readelf读取hello.o得到的ELF格式信息
| hello.elf
| 由hello可执行文件天生的ELF格式文件
|
(附件0分,缺失 -1分)
参考文献
[1] RANDALE.BRYANT, DAVIDR.O’HALLARON. 深入明确计算机系统[M]. 机械工业出版社, 2011.
[2]进程的创建过程-fork函数https://blog.csdn.net/lyl194458/article/details/79695110
[3] argc argv的概念https://baike.baidu.com/item/argc%20argv/10826112?fr=aladdin
[4] https://www.cnblogs.com/pianist/p/3315801.html
(参考文献0分,缺失 -1分)
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |