程序人生——Hello‘s P2P(HIT CSAPP大作业)
Posted 归元无一
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了程序人生——Hello‘s P2P(HIT CSAPP大作业)相关的知识,希望对你有一定的参考价值。
计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术
学 号 2021111282
班 级 2103101
学 生 张诚玮
指 导 教 师 刘宏伟
计算机科学与技术学院
2022年5月
本文从hello.c的视角出发,论述了hello.c从被程序员写出来到被预处理、编译、汇编、链接的经过以及其经过进程管理等一系列过程的经过与发生的变化。本文将联系实际操作的结果与计算机系统的知识对结果进行分析,找出输出是这样的原因,并且通过对程序从被写出来到可执行文件过程的分析,更进一步地认识和了解计算机的工作原理,程序在计算机当中的经过。能够更进一步地认识到如何才能写好代码。
关键词:预处理;编译;汇编;链接;进程管理。
目 录
6.2 简述壳Shell-bash的作用与处理流程 - 25 -
第1章 概述
1.1 Hello简介
1.1.1 P2P:From Program to Process
Program: 由程序员在编辑器(CodeBlocks、VS、VSC等)中输入代码形成的,一般被保存为xxx.c的程序。
Process: C程序经过预处理器(cpp)的预处理,编译器(ccl)的编译、汇编器(as)的汇编、链接器(ld)的链接最终形成的二进制可执行目标文件hello。通过往shell中输入启动命令的方式,shell将其fork成进程来运行。
图1.P2P的大体流程
1.1.2 O2O:From Zero-0 to Zero-0
shell通过execve在fork产生的子进程中加载hello,先删除当前虚拟地址的用户部分已存在的数据结构,为hello的代码段、数据、bss以及栈区域创建新的区域结构,然后映射虚拟内存,设置程序计数器,使之指向代码区域的入口点,进入程序入口后程序开始载入物理内存,而后进入main函数,CPU为hello分配时间片执行逻辑控制流。hello通过Unix I/O管理来控制输出。hello执行完成后shell父进程会回收hello进程,并且内核会从系统中删除hello所有痕迹,至此,hello完成O2O的过程[2]。
1.2 环境与工具
1.2.1 硬件环境
X64 CPU;2.30GHz;16G RAM;512GHD Disk
1.2.2 软件环境
Windows 10 64位;Vmware 16.2.4;Ubuntu 20.04
1.2.3 开发工具
vi/vim/gedit+gcc
1.3 中间结果
文件名称 | 文件作用 |
hello.i | 预处理之后的文本文件 |
hello.s | 编译之后的汇编文件 |
hello.o | 汇编之后的可重定位目标执行 |
hello | 链接之后的可执行目标文件 |
helloo.objdmp | hello.o的反汇编代码 |
helloo.elf | hello.o的ELF格式 |
hello.objdmp | hello的反汇编代码 |
hello.elf | hello的ELF格式 |
表1.各文件名称及其作用
1.4 本章小结
本章介绍了P2P、020的概念以及实验过程之中所用到的软硬件环境和开发与测试工具,总体地描述了通过高级语言C语言编写的代码hello.c经过预处理、编译、汇编、链接等流程的中间产物,以及对hello.c的P2P过程进行分析与处理时生成的.elf,.objdmp文件,从大体上介绍了本次实验。
第2章 预处理
2.1 预处理的概念与作用
2.1.1 预处理的概念
预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。典型地,由预处理器(preprocessor) 对程序源代码文本进行处理,得到的结果再由编译器核心进一步编译。这个过程并不对程序的源代码进行解析,但它把源代码分割或处理成为特定的单位——预处理记号(preprocessing token)用来支持语言特性[3]。
2.1.2 预处理的作用
预处理器主要实现下面四种功能:
1) 宏定义: #define指令定义一个宏,#undef指令删除一个宏定义。
2)文件包含: #include指令导致一个指定文件的内容被包含到程序中。
3) 条件编译:#if, #ifdef, #ifndef, #elif, #else 和 #dendif指令可以根据编译器测试的条件来将一段文本包含到程序中或者排除在程序之外。
4) 删除注释:不编译注释内容。
2.2在Ubuntu下预处理的命令
在Ubuntu的终端输入gcc -m64 -no-pie -fno-PIC -E hello.c -o hello.i以执行预处理操作,生成hello.i文件,如图2所示:
图2.预处理命令
生成的hello.i文件一共有3060行,只截取其中的一部分如图3所示:
图3.部分hello.i代码
2.3 Hello的预处理结果解析
如图3所示,经过预处理器的预处理,代码的行数增多了,由原来的24行转变为现在的3060行,而原来编写的代码中的注释被删去,包括的头文件变成了具体的指令,但是,hello.i并没有变成二进制文件,它只是对源文件文本的内容进行了一定程度的扩充与删减,使之从程序员能轻松理解的代码转变为机器更容易理解的代码,但是,本质上仍然是源代码的文本文件。
2.4 本章小结
本章讲述了程序P2P的第一个步骤,也就是程序预处理的步骤。本章详细地阐述了预处理的概念、作用,并且通过gcc指令在Ubuntu下生成了程序通过预处理后的文件hello.i并对结果进行了解析,大体上了解了系统预处理的原因,方式以及结果。
第3章 编译
3.1 编译的概念与作用
3.1.1 编译的概念
1、利用编译程序从源语言编写的源程序产生目标程序的过程。
2、用编译程序产生目标程序的动作。 编译就是把高级语言变成计算机可以识别的2进制语言,计算机只认识1和0,编译程序把人们熟悉的语言换成2进制的。
3、编译程序把一个源程序翻译成目标程序的工作过程分为五个阶段:词法分析;语法分析;语义检查和中间代码生成;代码优化;目标代码生成。主要是进行词法分析和语法分析,又称为源程序分析,分析过程中发现有语法错误,给出提示信息[4]。
3.1.2 编译的作用
将高级语言程序代码(C语言代码)翻译为汇编语言代码,把代码转化为汇编指令。把程序员更能理解的代码转变为机器更能理解的代码,并且以二进制的形式(显示为16进制)给出对应的指令、操作数等,有利于机器的执行。利用编译程序从源语言编写的源程序产生目标程序,把高级语言变成计算机可以识别的2进制语言。
3.2 在Ubuntu下编译的命令
在Ubuntu的终端输入gcc -m64 -no-pie -fno-PIC -S hello.c -o hello.i以执行编译操作,生成hello.s文件,如图4所示:
图4.hello.s
生成的hello.s文件共80行,现截取其中一部分代码,如图5所示:
图5.hello.s的部分代码
3.3 Hello的编译结果解析
3.3.1 代码的声明
如图5所示即为hello.s的开头代码,亦即为代码的部分声明,如表2所示:
代码 | 声明 |
.file | 源文件 |
.text | 代码段 |
.section .rodata | 只读数据段 |
.align | 对指令或者数据的存放地址进行对齐的方式 |
.string | 字符串 |
.global | 全局变量 |
.type | 制定函数类型或对象类型 |
表2.代码的部分声明
3.3.2 数据
3.3.2.1 常量
图6.rodata段
图6所示的即为汇编程序的只读数据段(.rodata)。其中,LC0和LC1分别对应了代码中的两个printf函数所对应的输出的文本的内容。由于第一个printf输出的为中文字符串,而hello.s是ASCII的文本文件,因此,不支持中文的显示,所以显示出来的是中文的UTF-8编码。
除此之外,hello.s中的其他常量一般以立即数的形式,在执行的过程之中被寄存器调用。
3.3.2.2 变量
观察hello.c源代码,不难发现,程序之中的变量有且仅有三种,分别是输入主函数的两种:int argv,char *argc[],主函数之中定义的一种:int i。其中,标准的用法下,argc有4个数据,因此,总共有6个局部非静态变量,分别为argv,argc[0],argc[1],argc[2],argc[3],i。
图7.main函数的汇编代码
图8.栈中的位置
观察图7,edi传递第一个参数,也就是argv,rsi传递第二个参数,也就是argc数组,其中,argc[0]中保存的是程序的路径以及名称,因此,没有必要再在栈中保存,argc与argv在栈中的位置如图8所示。
图9.循环变量i
由图9可以看出,通过比较立即数8与-4(%rbp)来对代码进行跳转的操作,可以看出,循环变量i被存放在-4(%rbp)的位置,被存放在栈中。这是因为i是在循环外定义的变量,若是放在循环内定义(比如for(int i = 0 ; i < 9;i ++)),则i会被放在寄存器中进行循环。
3.3.3 赋值操作
图10.赋值
如图10所示,对于变量i的赋值,一般采用直接将立即数赋值到相应的栈中的位置。用mov指令来对变量i进行赋值。
3.3.4 类型转化
在本函数中,只运用了一种类型的转化,那就是字符串向整形的转化,通过函数atoi来实现,如图11所示:
图11.类型的转化
3.3.5 算术操作
在本函数之中,只运用了一种算术操作那就是i++,实现的指令如图12所示:
图12.i++操作
通过add指令来对变量i所在位置进行+1操作。
3.3.6 关系操作
在本函数之中,运用了两次关系操作,分别是argc!=4和i<9。
在汇编语言中,关系操作一般是通过cmp指令与jmp指令的协作下来共同进行实现的,如图13,14所示,其分别为C语言中的两个表达式的语句:argc!=4,i<9,可以看到,在汇编之中,<优化成了<=。
图13.argc!=0
图14.i<9
在汇编语言中,一般通过两者相减来判断大小,根据相减的结果设置相应的条件码如ZF,SF等。
3.3.7 数组操作
在汇编语言之中,数组的表示其实就是变量数据的表示,只不过,数组表示的数组是一连串的连续的地址空间,因此,表现在栈中就是数组的数据被连续地压入栈中,而寄存器中记录的是数组的首地址。要访问其他的数组成员,则是根据数组的首地址加上偏移量后进行访问,如图15所示:
图15.循环体
如图是循环体内的操作(只有循环体内调用了argv数组),可以看到,通过movq -32(%rbp),%rax操作将数组的首地址赋值给了rax寄存器,再通过addq操作来控制数组的下标,从而读取对应的数据。
3.3.8 控制转移
在本函数之中,只出现了两种控制转移。一种是if语句,另外一种是for循环。两种语句都与关系操作相关,因此,相关的图被放在了3.3.6关系操作之中。
3.3.8.1 if判断
如图13所示是函数的if判断语句。根据关系操作后的条件码从而可以对je进行判断,从而实现语句的跳转,如果不相等,那么跳转到对应的函数体来进行语句的执行。
3.3.8.2 for循环
如图14所示是函数的for循环判断语句。根据关系操作后的条件码从而可以对jle进行判断,从而实现语句的跳转,如果小于等于,那么跳转到对应的函数体来进行语句的执行。
3.3.9 函数操作
函数的参数在汇编中通过寄存器来实现传递,其中返回值存储在eax中,参数则存储在rdi, rsi, rdx, rcx, r8, r9这六个寄存器,这六个寄存器分别表示第一、第二一直到第六个参数,如果六个寄存器无法满足函数的调用需求,则还需要利用栈帧将第七个及以上的参数存入栈中,函数要调用就从栈中进行调用,但本函数中没有用到。调用函数时用到的汇编语句是call,返回时则用ret。图11所示就是典型的对atoi函数的调用。在本程序之中,使用的函数包括printf,atoi,sleep,getchar等函数。
对于函数的调用,大都相似,即先将参数放在指定的寄存器中(或放在栈中),然后再通过call指令调用函数,转移到函数所在的地址进行操作,在操作过程中,返回值被赋值到了eax之中,最后通过ret函数进行返回。
3.4 本章小结
本章解释了程序编译过程中“编译”步骤的概念和作用,并通过hello.s这一例子展示了编译得到的汇编语言代码,从中可以看到从高级语言代码到汇编代码的翻译方式,也可以看出编译器并不是直接死板地翻译高级语言,而是要经过修整以适合机器的运作规律,对代码进行了一定程度的优化,从而使下一步翻译成二进制机器语言更加方便快捷。
第4章 汇编
4.1 汇编的概念与作用
4.1.1 汇编的概念
汇编器(as)将hello.s翻译为机器语言,产生可重定位目标程序,生成hello.o文件。hello.o文件是二进制文件。需要注意的是,此时的hello.o还未进行链接,所以不可直接运行。
4.1.2 汇编的作用
将hello.s 编译得到hello.o,再汇编得到机器语言二进制程序。
4.2 在Ubuntu下汇编的命令
在Ubuntu的终端输入gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o以执行编译操作,生成hello.o文件,如图16所示:
图16.hello.o
4.3 可重定位目标elf格式
如图17所示,是可重定位目标elf的格式:
图17.可重定位目标的格式
4.3.1 elf头
图18.hello.o的elf头
Elf头开始是一个16字节序列,前四个字节是elf格式固定的开头,然后的三个字节依次代表64位、小端序和文件头版本。Elf头含有文件的最基本信息,是在链接时读取并理解这个文件所必不可少的。
4.3.2 节头部表
图19.hello.o的节头部表
节头部表列出了各节的大小、类型、地址、偏移量等信息,方便查找各节。
4.3.3 符号表
图20.hello.o符号表
显示符号表段中的项。
4.3.4 可重定位段
图21.hello.o中的可重定位段
文件中有一些内存地址或引用,这些地方在链接前是待定的,需要视链接的情况指定确切的地址。因此,需要对这些地址进行重定位。每个代码段或数据段都对应一个重定位表,记录了段中的这些位置,方便对它们进行查找和操作。
4.4 Hello.o的结果解析
如图22所示,是hello.o文件的反汇编:
图22.hello.o的反汇编
与hullo.s相比,hello.o的反汇编主要有如下几点区别:
- 立即数。hello.s中的立即数都以十进制的形式进行保存,而hello.o的反汇编中的立即数大多都以十六进制的方式显示;
- 分支转移。在hello.s之中,跳转的分支以L2,L3之类的方式进行表示,而在hello.o的反汇编文件之中,大多采用以偏移量表示的地址,比如5e: R_X86_64_PLT32 printf-0x4;
- 函数调用。在hello.s之中,函数的调用一般是call puts@PLT之类的形式,而在hello.o的反汇编文件之中,一般是以地址加偏移量的形式表示的,比如callq 62 <main+0x62>。
这些区别,主要是由于机器码中没有符号的概念,所有的符号都要变成具体可查的数字,以供机器的理解与执行。但内存地址又是偏移量而不是具体的数值,这是因为还没有对文件进行链接,无法确定使用的内存地址,这部分要留给重定位来解决。
4.5 本章小结
本章解释了高级语言(C语言)程序编译过程中“汇编”步骤的概念和作用,并以hello.o的elf格式和反汇编代码为例展示了二进制机器语言文件的格式和特性。从汇编代码到二进制机器语言的过程是有迹可循、有理可依的,它为下一步的链接又提供了更方便的条件,为程序员所写的代码被机器所能理解打下了坚实的基础,迈出了重要的一步。
第5章 链接
5.1 链接的概念与作用
5.1.1 链接的概念
链接是结合多个不同的可重定位目标文件、得到具有统一内存地址,能够运行的可执行程序的过程。一个复杂软件分为很多的模块,人们把每个模块独立地编译,然后按需组装起来的过程就是链接。链接将不同文件中的数据和程序段结合统一起来,在编程时方便由各个小文件组成大型程序,条理清晰,使得更加分散化、模块化的编程成为可能。链接主要包括地址和空间分配、符号决议(也叫符号绑定、名称绑定、地址绑定)、重定位等步骤。
5.1.2 链接的作用
链接的存在可以让程序分离编译,然后链接就将分离的目标文件、启动代码、库文件等链接成可执行文件。
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
的命令对hello.o进行链接,如图23所示:
图23.对hello.o进行链接
5.3 可执行目标文件hello的格式
通过指令readelf -a hello > hello.elf 将hello的elf格式输出到文件hello.elf之中,如图24所示:
图24.输出到hello.elf
5.3.1 Elf头
如图25所示是hello的elf格式的Elf头:
图25.Elf头
5.3.2 节头部表
如图26所示为hello的elf格式的部分节头部表信息:
图26.节头部表信息
5.3.2 程序头表
如图27所示为hello的elf格式的程序头表信息:
图27.程序头表信息
5.3.2 符号表
如图28所示为hello的elf格式的部分符号表信息:
图28.部分符号表信息
5.4 hello的虚拟地址空间
命令行输入edb,打开后,将hello.ld可执行文件拖入edb界面,查看左下角Data Dump一栏,如图29所示,即为edb界面的Data Dump栏,如图30所示,即为edb界面的memory regions界面:
图29.edb界面的Data Dump栏
图30.edb界面的Memory Regions
可以看出,程序在0x00401000 ~ 0x00402000段中,虚拟地址从0x00401000开始,到0x00401ff0结束。
5.5 链接的重定位过程分析
使用指令:objdump -d -r hello > hello.objdump将重定位项目的内容写到文件hello.objdump当中。图31为部分hello.objdump,图32为部分helloo.objdump。
图31.hello.objdump
图32.helloo.objdump
主要有以下两项区别:
- 以0开头的虚拟地址变成了具体的内存地址;
- 函数的调用也变成了内存地址;增加了.init和.plt节;增加了getchar等库函数。
由此可知,链接的过程就是将不同.o文件的内容按合理顺序拼接在一起使得彼此能够配合的过程。在重定位时,链接器需要整理符号表中的条目,分配出内存地址。先将每个同类节合并成同一个节,然后为它们赋予内存地址,使指令和变量有唯一的内存地址。最后将重定位节中的符号引用改为内存地址。
5.6 hello的执行流程
函数调用如下表格所示:
地址 | |
ld-2.23.so!_dl_start | 0x00007f8dec5b79b0 |
ld-2.27.so! dl_init | 0x00007f8dec5c6740 |
hello!_start | 0x004004d0 |
ld-2.27.so!_libc_start_main | 0x00400480 |
libc-2.27.so! cxa_atexit | 0x00007f8dec226280 |
hello!_libc_csu_init | 0x00400580 |
hello!_init | 0x00400430 |
libc-2.27.so!_setjmp | 0x00007f8dec221250 |
libc-2.27.so!_sigsetjmp | 0x00007f8dec221240 |
libc-2.27.so!__sigjmp_save | 0x00007fa8dec221210 |
hello_main | 0x004004fa |
hello!puts@plt | 0x00400460 |
hello!exit@plt | 0x004004a0 |
hello!printf@plt | 0x00400470 |
hello!sleep@plt | 0x004004b0 |
hello!getchar@plt | 0x00400490 |
ld-2.23.so!_dl_runtime_resolve_avx | 0x00007f8dec5cd870 |
libc-2.27.so!exit | 0c00007f6002de35b0 |
表3.函数的调用
5.7 Hello的动态链接分析
动态链接项目中,查看dl_init前后项目变化。对于动态共享链接库中PIC函数,编译器加重定位记录,等待动态链接器处理,为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略,将过程地址的绑定推迟到第一次调用该过程。动态链接器使用过程链接表PLT+全局偏移量表GOT实现函数的动态链接,GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。
在dl_init调用之前,对于每一条PIC函数调用,调用的目标地址都实际指向PLT中的代码逻辑,初始时每个GOT条目都指向对应的PLT条目的第二条指令。
图33.Data Dump
在dl_init调用之后, 0x6008c0和0x6008c0处的两个8字节的数据分别发生改变。
和PLT联合使用时,GOT[0]和GOT[1]包含动态链接器在解析函数地址时会使用的信息。其中GOT[1]指向重定位表(依次为.plt节需要重定位的函数的运行时地址)用来确定调用的函数地址, GOT[2]是动态链接器ld-linux.so模块中的入口点。
在之后的函数调用时,首先跳转到PLT执行.plt中逻辑,第一次访问时,GOT地址为下一条指令,将函数序号压栈,然后跳转到PLT[0],在PLT[0]中将重定位表地址压栈,然后访问动态链接器,在动态链接器中使用函数序号和重定位表确定函数运行时地址,重写GOT,再将控制传递给目标函数。之后如果对同样函数调用,第一次访问跳转直接跳转到目标函数。
5.8 本章小结
本章简述了链接的概念与作用,分析了经过链接生成的hello文件的结构以及与之前经过链接的hello.o文件的异同,分析了hello文件的运行流程,使用edb探索了动态链接的过程。经过链接,hello.c已经从程序员所写的源代码程序转变成了机器可以理解并且执行的可执行文件。至此,就是Hello的诞生。
第6章 hello进程管理
6.1 进程的概念与作用
6.1.1 进程的概念
1.进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。
2.进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时(操作系统执行之),它才能成为一个活动的实体,我们称其为进程。
6.1.2 进程的作用
进程能够提供给应用程序一些关键抽象:
1) 一个独立的逻辑控制流。进程使得我们感觉好像在独占处理器。
2) 一个私有地址空间。进程使得我们感觉好像独占地使用内存系统。
6.2 简述壳Shell-bash的作用与处理流程
6.2.1 Shell的概念与作用
Linux系统中,Shell是一个交互型应用级程序,代表用户运行其他程序(是命令行解释器,以用户态方式运行的终端进程)。
6.2.2 Shell的处理流程
其基本功能是解释并运行用户的指令,重复如下处理过程:
(1)终端进程读取用户由键盘输入的命令行。
(2)分析命令行字符串,获取命令行参数,并构造传递给execve的argv向量
(3)检查第一个(首个、第0个)命令行参数是否是一个内置的shell命令
(3)如果不是内部命令,调用fork( )创建新进程/子进程
(4)在子进程中,用步骤2获取的参数,调用execve( )执行指定程序。
(5)如果用户没要求后台运行(命令末尾没有&号)否则shell使用waitpid(或wait...)等待作业终止后返回。
(6)如果用户要求后台运行(如果命令末尾有&号),则shell返回;
6.3 Hello的fork进程创建过程
在命令行输入 ./hello执行 hello 程序时,由于hello不是内部命令,所以shell会fork一个子进程并进行后续操作。
新建的子进程几乎和父进程相同。子进程拥有与父进程用户级虚拟地址空间相同且独立的一份副本,与父进程任何打开的文件描述符相同的副本。
使用fork()函数来创建一个子进程,fork函数的原型为:pid_t fork(void)
fork()函数有以下特点:
1)调用一次,返回两次。一次返回至父进程,返回的是子进程的pid;一次返回至子进程返回值为0。
2) 并发执行。父子进程是并发运行的独立进程。
3) 相同但独立的地址空间。子进程创建时,两个进程具有相同的用户栈、本地变量、堆、全局变量、代码。但是二者对这行的改变都是相互独立的。
4) 共享文件。
6.4 Hello的execve过程
使用fork创建进程后,子进程便会使用execve加载并运行hello程序,且带参数列表argv以及环境变量envp。execve调用一次,从不返回。
图34.参数列表与环境变量列表
观察可知,argv指向一个指针数组,这个指针数组中的每一个指针指向一个参数字符串。其中argv[0]使我们所运行的程序的名字。envp指向一个指针数组,这个数组里面的每一个指针指向一个环境变量的字符串。环境变量字符串的格式为”name = value”。使用getenv函数获取环境变量,setenv、unsetenv来设置、删除环境变量。
execve会调用启动加载器。加载器会删除子进程现有的虚拟内存段,创建一组新的代码、数据、堆、栈。新的栈和堆被初始化为0。通过虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码数据初始化。最后,跳转到_start地址,最终调用main函数。
6.5 Hello的进程执行
系统中每个程序都运行在某个进程的上下文中。上下文是程序正确运行所需要的状态,由系统内核维持。
一个运行多个进程的系统,进程逻辑流的执行可能是交错的。每个进程执行它的流的一部分, 然后被抢占,轮到其他进程执行。一个逻辑流在时间上与另一个重叠ÿ
以上是关于程序人生——Hello‘s P2P(HIT CSAPP大作业)的主要内容,如果未能解决你的问题,请参考以下文章