计算机系统

大作业

题 目 程序人生-Hello’s P2P
专 业 计算机系
学   号 1170300921
班   级 1703009
学 生 ***    
指 导 教 师 史先俊

计算机科学与技术学院
2018年12月
摘 要
本文主要介绍了hello程序的预处理、编译、汇编、链接以及hello运行是的进程、信号、异常处理、存储处理,介绍了有关进程、内存、IO管理的相关知识,旨在阐述程序从建立到执行的所有过程,结合计算机系统的知识,进行详细解释。

关键词:代码、预处理、编译、汇编、链接、运行、创建子进程、运行程序、指令、内存、信号

(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)

目 录

第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简介
在电脑中写入代码得到Hello.c程序(program),将其用cpp预处理,ccl编译,as汇编,ld链接,最终成为可执行程序,然后在shell中启动,shell将该程序fork,产生子进程,变成process,这就是P2P的过程。然后execve,映射虚拟内存,然后载入物理内存,然后执行目标代码,CPU为hello分配时间片执行逻辑控制流。运行结束后,父进程回收hello子进程,内核执行删除操作,这就是O2O。
1.2 环境与工具
硬件环境:Intel®Core™i7-6700HQ CPU@2.60GHz 2.59GHz 8G RAM 64位操作系统 128SSD+1T HDD。
软件环境:Ubuntu
开发与调试:vim,gcc,as,ld,ebd,readelf,HexEdit
1.3 中间结果
文件名称 文件作用
Hello.i 预处理之后文本文件
Hello.s 编译之后的汇编文件
Hello.o 汇编之后的可重定位目标执行
Hello 链接之后的可执行文件
Hello2.c 测试程序代码
Hello2 测试程序
Helloo.objdmp Hello.o的反汇编代码
Helloo.elf Hello.o的ELF格式
Hello.objdmp hello的反汇编代码
Hello.elf Hellode ELF格式
Tmp.txt 存放临时数据

1.4 本章小结
主要介绍了hello的p2p,o2o过程,实验所需的环境与工具,列出了中间结果文件的名字和作用
(第1章0.5分)

第2章 预处理
2.1 预处理的概念与作用
概念:预处理器(cpp)根据以字符#开头的命令,修改原始的C程序,将所有库变成一个文本文件,包括:宏定义,文件包含,条件编译。通常以.i作为文件扩展名。
作用:1.将#include的声明放到新程序中。比如hello.c中第一行#include <stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中。
2.宏定义:将符号常量替换成文本。
3.文件包含:把多个源文件连接成一个源文件进行编译,结果将生成一个目标文件。
4.条件编译允许只编译源程序中满足条件的程序段,从而减少内存的开销,提高程序效率。
5.有助于程序的修改,阅读,移植和调试,便于代码模块化。

2.2在Ubuntu下预处理的命令
命令:cpp hello.c > hello.i

图2-1 预处理命令

图2-2 生成hello.i文件
2.3 Hello的预处理结果解析

图2-3 预处理的hello.i文件(部分)
分析:通过预处理,hello.c文件变为hello.i文件,原文件进行了宏展开,stdio.h,unistd.h,stdlib.h依次展开,文件扩展为3188行。main函数出现在3102行,如图。cpp通过#ifdef,#ifndef对条件值来判断是否执行。
2.4 本章小结
介绍了预处理的概念和功能,并在Ubuntu下的预处理指令将hello.c转化为hello.i文件,并对其进行了分析。

(第2章0.5分)

第3章 编译
3.1 编译的概念与作用
概念:编译器(ccl)将文本文件hello.i翻译成文本文件hello.s包含一个汇编语言程序。
作用:1:概念
2:语法检查、调试措施、修改手段、覆盖处理、目标程序优化、不同语言合用、人及联系。

3.2 在Ubuntu下编译的命令
命令:gcc -S hello.i -o hello.s

图3-1 Ubuntu下的编译命令生成hello.s文件
3.3 Hello的编译结果解析
汇编指令:
指令 含义
.file 声明源文件
.text 以下是代码段
.section.rodata 以下是rodata节
.globl 声明一个全局变量
.type 指定函数类型和对象类型
.size 声明大小
.long .string 声明一个long,string类型
.align 声明对指令或者数据的存放地址进行对齐的方式

3.3.1 数据
(1)、字符串:在.rodata中只读数据节
1.“Usage:hello 学号 姓名!\n”字符串变为UTF-8格式,汉字占三个字节,一个\占一个字节。如图;
2.“hello %s %s\n”,具体如图:
图3-2 hello.s声明在LC0和LC1中的字符串
(2)、整数
1.int sleepsecs:sleepsecs被声明为全局变量,已经被赋值过了,编译器在.data声明该变量。在.data中,对齐方式为4,设置类型为对象,设置大小为4字节,设置为Long型为2。
图3-3 hello.s中的sleepsecs的声明
2.Int i:编译器在hello.s中将i存储在栈空间-4(%rbp)中,占了栈的4B。
3.Int argc:传入的第一个参数
4.其他整型数据都是以立即数的性数出现
(3)、数组
Char *argv[]main,执行函数时的输入命令行。在hello.s中使用两次(%rax)(两次rax分别为argv[1]和argv[2]的地址)取出其值,如图:
图3-4 计算地址取出数组
3.3.2赋值
(1)、int sleepsecs=2.5:sleepsecs是全局变量,所以在.data中将sleepsecs声明为2的long型。
(2)、i=0:整形赋值使用mov指令完成,大小不同有区别:
指令 B W l Q
大小 8b(1B) 16b(2B) 32b(4B) 64b(8B)

i是4B的int类型,所以用movl赋值,如图:

图3-5 变量i的赋值
3.3.3 类型转化
Int sleepsecs=2.5中,将浮点类型的2.5转换为int类型。
浮点数类型默认为double,所以是double强制转化为int类型,遵循向零舍入的原则,将2.5舍入为2。

3.3.4 算术操作
有如下汇编指令:
指令 效果
Leaq S,D D=&S
INC D D+=1
DEC D D-=1
NEG D D=-D
ADD S,D D=D+S
SUB S,D D=D-S
IMULQ S R[%rdx]:R[%rax]=SR[%rax](有符号)
MULQ S R[%rdx]:R[%rax]=S
R[%rax](无符号)
IDIVQ S R[%rdx]=R[%rdx]:R[%rax] mod S(有符号)
R[%rax]=R[%rdx]:R[%rax] div S
DIVQ S R[%rdx]=R[%rdx]:R[%rax] mod S(无符号)
R[%rax]=R[%rdx]:R[%rax] div S

程序中的算数操作:
1.i++,i自增,使用指令addl,l为后缀(4B)。
2.汇编中leaq.LC1(%rip),%rdi,使用了加载有效地址指令leaq计算LC1的段地址%rip+LC1并传递给%rdi。

3.3.5 关系操作
有如下指令:
指令 效果 描述
CMP S1,S2 S2-S1 比较-设置条件码
TEST S1,S2 S1&S2 测试-设置条件码
SET** D D=** 按照将条件码设置D
J
—— 根据**与条件码进行跳转
程序中涉及的关系运算为: 1) argc!=3:判断 argc 不等于 3。hello.s 中使用 cmpl $3,-20(%rbp),计算 argc-3 然后设置条件码,为下一步 je 利用条件码进行跳转作准备。 2) i<10:判断i小于10。hello.s中使用cmpl $9,-4(%rbp),计算 i-9 然后设置 条件码,为下一步 jle 利用条件码进行跳转做准备。

3.3.6 控制转移
涉及的控制转移:(1) if (argv!=3):当 argv 不等于 3 的时候执行程序段中的代码。如图,首先 cmpl 比较 argv 和 3,设置条件码,使用 je 判断 ZF 标志位,如果为 0,说明 argv-3=0 argv==3,则直接跳转到.L2,否则顺序执行下一条语句。

图3-6 if语句的编译
2)for(i=0;i<10;i++);使用计数变量i循环 10 次。如图,首先无条件跳转到位于循环体.L4 之后的比较代码,如果 i<=9,则跳入.L4 for 循环体执行,否则循环结束,执行 for 之后的逻辑。

图3-7 for循环的编译

3.3.7 函数操作
函数是一种过程,用一组指定的参数和可选的返回值实现某种功能。P调用函数包含以下动作:传递控制、传递数据、分配和释放内存。
64 位程序参数存储顺序:
1 2 3 4 5 6 7
%rdi %rsi %rdx %rcx %r8 %r9 栈空间
程序中涉及函数操作的有:
1)main函数:a)传递控制,main函数因为被调用call才能执行,call指令将下一条指令的地址dest压栈,然后跳转到main函数。b) 传递数据,外部调用过程向main函数传递参数 argc 和 argv,使用%rdi和%rsi存储,函数正常出口为 return 0,将%eax 设置 0 返回。 c) 分配和释放内存,使用%rbp记录栈帧的底,函数分配栈帧空间在%rbp之上,程序结束时,调用leave指令,leave 相当于mov %rbp,%rsp,pop%rbp,恢复栈空间为调用之前的状态,然后ret返回,ret相当 pop IP,将下一条要执行指令的地址设置为dest。
2)printf 函数: a) 传递数据:先 printf 将%rdi 设置为“Usage: Hello 学号姓名!\n”字符串的首地址。再将printf设置%rdi为“Hello %s %s\n” 的首地址,设置%rsi为argv[1],%rdx为argv[2]。b)控制传递:第一次 printf 因为只有一个字符串参数,call puts@PLT;第二次printf使用call printf@PLT。
3)exit 函数:a) 传递数据:将%edi设置为 1。b) 控制传递:call exit@PLT。
4)sleep 函数:传递数据:将%edi 设置为sleepsecs。 控制传递:call sleep@PLT。
5)getchar 函数:控制传递:call gethcar@PLT
3.4 本章小结
本节完成了对hello.i的编译工作,成为汇编语言,分别介绍了编译的数据、赋值、类型转化、算数操作、关系操作、控制转移、函数操作及相关函数,解释了以上操作的原理,然后根据hello.s来解释,最终完全解释了汇编代码。
第4章 汇编
4.1 汇编的概念与作用
汇编程序是把汇编语言书写的程序翻译成与之等价的机器语言程序的翻译程序。汇编程序输入的是用汇编语言书写的源程序,输出的是用机器语言表示的目标程序。汇编语言的指令与机器语言的指令大体上保持一一对应的关系,汇编算法采用的基本策略是简单的。通常采用两遍扫描源程序的算法。第一遍扫描源程序根据符号的定义和使用,收集符号的有关信息到符号表中;第二遍利用第一遍收集的符号信息,将源程序中的符号化指令逐条翻译为相应的机器指令。
4.2 在Ubuntu下汇编的命令
指令:as hello.s -o hello.o

图4-1 生成hello.o文件
4.3 可重定位目标elf格式
使用 readelf -a hello.o > helloo.elf 指令获得 hello.o 文件的 ELF 格式。组成如下:
1)ELF Header:以Magic开始,Magic 描述了生成该文件的系统的字的大小和字节顺序,ELF 头剩下的部分包括ELF头的大小、目标文件的类型、机器类型、字节头部表(section header table)的文件偏移,以及节头部表中条目的大小和数量等信息。
图4-2 ELF Header
2)Section Headers:节头部表,包含了文件中各个节的语义,包括节的类型、位置和大小等。
图4-3 节头部表Section Headers
3)重定位节.rela.text ,包含.text节中需要进行重定位的信息,目标文件和其他文件组合时,需要修改这些位置。如图 4.4,图中 8 条重定位信息分别是对.L0、puts 函数、exit 函数、.L1、printf 函数、 sleepsecs、sleep函数、getchar 函数进行重定位声明。
4)图4-4重定位节.rela.text
4.4 Hello.o的结果解析
命令:objdump -d -r hello.o > helloo.objdump

图4-5 hello.s的反汇编代码
与第三章的图3-7进行对比,主要有如下差别:
1)分支转移:反汇编代码跳转指令的操作数使用的不是段名称如,段名称是汇编语言中便于编写的助记符,所以在汇编成机器语言之后是确定的地址。
2)函数调用:在.s 文件中,函数调用之后直接跟着函数名称,而在反汇编程序中,call的目标地址是当前下一条指令。因为 hello.c 中调用的函数需要通过动态链接器才能确定函数的运行时执行地址,在汇编成为机器语言时,将其call指令后的相对地址设置为全0,然 后在.rela.text 节中为其添加重定位条目。
3)全局变量访问:在.s文件中,访问 rodata,使用段名称+%rip,在反汇编代码中 0+%rip,在汇编成为机器语言时,将操作数设置为全0并添加重定位条目。
机器语言程序的是二进制的机器指令序列集合,是纯粹的二进制数据表示的语言,是电脑可以真正识别的语言。机器指令由操作码和操作数组成。汇编语言是以人们比较熟悉的词句直接表述CPU动作形成的语言,是最接近CPU运行原理的较为通俗的比较容易理解的语言。在不同的设备中,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令。机器语言与汇编语言具有一一对应的映射关系,一条机器语言程序对应一条汇编语言语句,但不同平台之间不可直接移植。
4.5 本章小结
完成了hello.s到hello.o的汇编,转化为了可重定位文件,将hello.o的elf格式和通过objdump得到的反汇编与.s汇编程序代码进行比较,了解了差别。
(第4章1分)

第5章 链接
5.1 链接的概念与作用
链接程序将分别在不同的目标文件中编译或汇编的代码收集到一个可直接执行的文件中。它还连接目标程序和用于标准库函数的代码,以及连接目标程序和由计算机的操作系统提供的资源。链接工作大致包含两个步骤,一是符号解析,二是重定位。在符号解析步骤中,链接器将每个符号引用与一个确定的符号定义关联起来。将多个单独的代码节和数据节合并为单个节。将符号从它们的在.o文件的相对位置重新定位到可执行文件的最终绝对内存位置。更新所有对这些符号的引用来反映它们的新位置。
5.2 在Ubuntu下链接的命令
命令:ld -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 /usr/lib/gcc/x86_64-linux-gnu/7/crtbegin.o /usr/lib/gcc/x86_64-linux-gnu/7/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o hello.o -lc -z relro -o hello

图5-1 使用 ld 命令链接生成可执行程序 hello
5.3 可执行目标文件hello的格式
命令:readelf -a hello > hello.elf 生成hello的elf文件

图5-2 hello文件的文件头
根据文件头的信息,可以知道该文件是可执行目标文件,有31个节。

在 ELF 格式文件中,节头对 hello 中所有的节信息进行了声明,包括大小 S以及在程序中的偏移量,因此根据节头中的信息我们就可以用 HexEdit 定位各个节所占的区间(起始位置,大小)。其中 Address 是程序被载入到虚拟地址的起始地址。
图5-3 hello ELF格式的节头表

由于是可执行目标文件,所以每个段的起始地址都不相同,它们的起始地址分别对应着装载到虚拟内存中的虚拟地址。这样可以直接从文件起始处得到各段的起始位置,以及各段所占空间的大小。同时可以观察到,代码段是可执行的,但是不能写;数据段和只读数据段都不可执行,而且只读数据段也不可写。
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,
图5-4 使用edb查看.txet段
如图 5-5,每一个项提供了各段在虚拟地址空间和物理地址空间的大小、位置、标志、访问权限和对齐方面的信息。在下面可以看出,程序包含 8 个段:
1)PHDR 保存程序头表。
2)INTERP 指定在程序已经从可执行文件映射到内存之后,必须调用的解释 器。
3)LOAD 表示一个需要从二进制文件映射到虚拟地址空间的段。
4)DYNAMIC 保存由动态链接器使用的信息。
5)NOTE 保存辅助信息。
6)GNU_STACK:权限标志,标志栈是否是可执行的。
7) GNU_RELRO:指定在重定位结束之后那些内存区域是需要设置只读。
图5-5 ELF文件的程序头

5.5 链接的重定位过程分析
使用 objdump -d -r hello > hello.objdump 获得 hello 的反汇编代码。
图5-6 反汇编文件

与hello.o的反汇编文件相比,多出了以下内容:
节名 描述
.interp 保存ld.so的路劲
.note.ABI-tag linux下特有的节
.hash 符号的哈希表
.gun.hash GNU拓展的符号的哈希表
.dynsym 运行时动态符号表
.dynstr 存放.dynsym节中的符号名称
.gnu.version 符号版本
.gnu.version_r 符号引用版本
.rela.dyn 运行时动态重定位表
.rela.plt .plt节的重定位条目
.init 程序初始化需要执行的代码
.plt 动态链接-过程链接表
.fini 当程序正常终止时需执行的代码
.eh_frame Contains exception unwinding and source language information.
.dynamic 存放被ld.so使用的动态链接信息
.got 动态链接-全局偏移量表-存放变量
.got.plt 动态链接-全局偏移量表-存放函数
.data 初始化的数据
.comment 一串包含编译器的NULL-terminated字符串

比较分析链接器如下:
1)函数个数:在使用 ld 命令链接的时,主要定义了main 函数,libc.so中定义了 hello.c 中的 printf、sleep、getchar、exit 函数和_start 中的 __libc_csu_init,__libc_csu_fini,__libc_start_main。链接器将上述函数加入。
2)函数调用:链接器解析重定条目时发现对外部函数调用的类型为 R_X86_64_PLT32 的重定位,此时.text 与.plt 节相对距离已经确定,链接器计算相对距离,将对动态链接库中函数的调用 值改为 PLT 中相应函数与下条指令的相对地址,指向对应函数。对于此类重定位 链接器为其构造.plt 与.got.plt。
3).rodata 引用:链接器解析重定条目时发现两个类型为 R_X86_64_PC32 的 对.rodata 的重定位(printf 中的两个字符串),由(2)知.rodata 与.text 节之间的相对距离确定,所以链接器直接修改 call 之后的值为目标地址与下一条指令的地址之差,指向相应的字符串。例:
refptr = s + r.offset = Pointer to 0x40054A
refaddr=ADDR(s)+r.offset=ADDR(main)+r.offset=0x400532+0x18=0x40054A
*refptr = (unsigned) (ADDR(r.symbol) + r.addend-refaddr) = ADDR(str1)+r.addend-refaddr=0x400644+(-0x4)-0x40054A=(unsigned)0xF6
5.6 hello的执行流程
加载程序 ld-2.23.so!_dl_start ld-2.23.so!_dl_init LinkAddress!_start libc-2.23.so!_libc_start_main libc-2.23.so!_cxa_atexit LinkAddress!_libc_csu.init libc-2.23.so!_setjmp
Call main LinkAddress!main
程序终止 libc-2.23.so!exit

5.7 Hello的动态链接分析
对于动态共享链接库中 PIC 函数,编译器没有办法预测函数的运行时地址,所 以需要添加重定位记录,等待动态链接器处理,为避免运行时修改调用模块的代 码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表 PLT+全局偏移量 表 GOT 实现函数的动态链接,GOT 中存放函数目标地址,PLT 使用 GOT 中地址 跳转到目标函数。 在 dl_init 调用之前,对于每一条 PIC 函数调用,调用的目标地址都实际指向 PLT 中的代码逻辑,GOT 存放的是 PLT 中函数调用指令的下一条指令地址。如在 图 5.7 (a)。 在 dl_init 调用之后,如图 5.7 (b),
图5-7(a) 没有调用 dl_init 之前的全局偏移量表.got.plt

图5-7(b) 调用 dl_init 之后的全局偏移量表.got.plt
在之后的函数调用时,首先跳转到 PLT 执行.plt 中逻辑,第一次访问跳转时 GOT 地址为下一条指令,将函数序号压栈,然后跳转到 PLT[0],在 PLT[0]中将重 定位表地址压栈,然后访问动态链接器,在动态链接器中使用函数序号和重定位 表确定函数运行时地址,重写 GOT,再将控制传递给目标函数。之后如果对同样函数调用,第一次访问跳转直接跳转到目标函数。
5.8 本章小结

本章讨论了链接过程中对程序的处理。Linux系统使用可执行可链接格式,即ELF,具有.text,.rodata等节,并且通过特定的结构组织。
经过链接,ELF可重定位的目标文件变成可执行的目标文件,链接器会将静态库代码写入程序中,以及动态库调用的相关信息,并且将地址进行重定位,从而保证寻址的正确进行。静态库直接写入代码即可,而动态链接过程相对复杂一些,涉及共享库的寻址。
链接后,程序便能够在作为进程通过虚拟内存机制直接运行。
总体来说,主要分析了 hello 的 虚拟地址空间、重定位过程、执行流程、动态链接过程。
(第5章1分)

第6章 hello进程管理
6.1 进程的概念与作用
1.概念
进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。

2.作用
进程作为一个执行中程序的实例,系统中每个程序都运行在某个进程的上下文中,上下文是由程序正确运行所需的状态组成的。
这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
6.2 简述壳Shell-bash的作用与处理流程
Shell 的作用:Shell 是一个用 C 语言编写的程序,他是用户使用 Linux 的桥梁。 Shell 是指一种应用程序,Shell 应用程序提供了一个界面,用户通过这个界面访问 操作系统内核的服务。
处理流程:
1)从终端读入输入的命令。
2)将输入字符串切分获得所有的参数
3)如果是内置命令则立即执行
4)否则调用相应的程序为其分配子进程并运行
5)shell 接受键盘输入信号,并对这些信号进行相应处理
6.3 Hello的fork进程创建过程
shell作为父进程通过fork函数为hello创建一个新的进程,供其执行。通过fork函数,子进程得到与父进程用户级虚拟地址空间相同的但是独立的一份副本。
在终端中键入 ./hello 1170300921 wangjiangrui,运行的终端程序会对输入的命令行进行解析,因为 hello 不是一个内置的 shell 命令所以解析之后终端 程序判断./hello 的语义为执行当前目录下的可执行目标文件 hello,之后终端程序 首先会调用 fork 函数创建一个新的运行的子进程,新创建的子进程几乎但不完全 与父进程相同,子进程得到与父进程用户级虚拟地址空间相同的(但是独立的) 一份副本,这就意味着,当父进程调用 fork 时,子进程可以读写父进程中打开的 任何文件。父进程与子进程之间最大的区别在于它们拥有不同的 PID。 父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的 逻辑控制流的指令。在子进程执行期间,父进程默认选项是显示等待子进程的完成。
shell通过fork进行进程创建的代码
if ((pid = fork()) < 0)
unix_error(“fork error”);

/*

  • Child process

*/

if (pid == 0)
{
/* Child unblocks signals */
sigprocmask(SIG_UNBLOCK, &mask, NULL);

/* Each new job must get a new process group IDso that the kernel doesn't send ctrl-c and ctrl-zsignals to all of the shell's jobs */
if (setpgid(0, 0) < 0)unix_error("setpgid error");/* Now load and run the program in the new job */
if (execve(argv[0], argv, environ) < 0)
{printf("%s: Command not found\n", argv[0]);exit(0);
}

}
6.4 Hello的execve过程
使用execve就是一次系统调用,首先要做的将新的可执行文件的绝对路径从调用者(用户空间)拷贝到系统空间中。在得到可执行文件路径后,就找到可执行文件打开,由于操作系统已经为可执行文件设置了一个数据结构,就初始化这个数据结构,保存一个可执行文件必要的信息。可执行文件不是真正上能够自己运行的,需要有代理人来代理。在系统内核中有一个formats队列,循环遍历这个队列,看看现在被初始化的这个数据结构是哪个代理人可以代理的。如果没有就继续查看数据结构中的信息。按照系统配置了是否可以动态加载模块,加载一次模块,再循环遍历看是否有代理人前来认领。找到正确的代理人后,代理人首先要做的就是放弃以前从父进程继承来的资源。主要是对信号处理表,用户空间和文件大资源的处理。将父进程的信号处理表复制过来,放弃原来的用户空间。然后载入真正的程序代码和数据段,开辟堆栈,映射执行参数和环境变量。
在execve加载了可执行程序之后,它调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数,即可执行程序的main函数。此时用户栈已经包含了命令行参数与环境变量,进入main函数后便开始逐步运行程序。
6.5 Hello的进程执行
上下文信息:上下文就是内核重新启动一个被抢占的进程所需要的状态,它由 通用寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内 核数据结构等对象的值构成。
时间片:一个进程执行它的控制流的一部分的每一时间段叫做时间片。
调度:
多个流并发地执行的一般现象被称为并发。一个进程和其他进轮流运行的概念称为多任务。一个进程执行它的控制流的一部分的每一时间段叫做时间片。因此,多任务也叫做时间分片。
hello程序执行过程中同样存储时间分片,与操作系统的其他进行并发运行。并发执行涉及到操作系统内核采取的上下文交换策略。内核为每个进程维持一个上下文,上下文就是内核重新启动一个先前被抢占的进程所需的状态。
在执行过程中,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程,这个过程称为调度。
如图 6-1,hello 初始运行在用户模式,在 hello 进程调用 sleep 之后陷入内核模 式,内核处理休眠请求主动释放当前进程,并将 hello 进程从运行队列中移出加入 等待队列,定时器开始计时,内核进行上下文切换将当前进程的控制权交给其他 进程,当定时器到时时发送一个中断信号,此时进入内核状态执行中断 处理,将 hello 进程从等待队列中移出重新加入到运行队列,成为就绪状态,hello 进程就可以继续进行自己的控制逻辑流了。
当 hello 调用 getchar 的时候,实际落脚到执行输入流是 stdin 的系统调用 read, hello 之前运行在用户模式,在进行 read 调用之后陷入内核,内核中的陷阱处理程 序请求来自键盘缓冲区的 DMA 传输,并且安排在完成从键盘缓冲区到内存的数据 传输后,中断处理器。此时进入内核模式,内核执行上下文切换,切换到其他进 程。当完成键盘缓冲区到内存的数据传输时,引发一个中断信号,此时内核从其 他进程进行上下文切换回 hello 进程。
图6-1 hello 进程 sleep 上下文切换的原理示意
6.6 hello的异常与信号处理
hello执行过程中可能出现四类异常:中断、陷阱、故障和终止。
中断是来自I/O设备的信号,异步发生,中断处理程序对其进行处理,返回后继续执行调用前待执行的下一条代码,就像没有发生过中断。
陷阱是有意的异常,是执行一条指令的结果,调用后也会返回到下一条指令,用来调用内核的服务进行操作。帮助程序从用户模式切换到内核模式。
故障是由错误情况引起的,它可能能够被故障处理程序修正。如果修正成功,则将控制返回到引起故障的指令,否则将终止程序。
终止是不可恢复的致命错误造成的结果,通常是一些硬件的错误,处理程序会将控制返回给一个abort例程,该例程会终止这个应用程序。
hello执行过程中,可能会遇到各种异常,信号则是一种通知用户异常发送的机制。例如较为底层的硬件异常以及较高层的软件事件,比如Ctrl-Z和Ctrl-C,分别触发SIGCHLD和SIGINT信号。
收到信号后进程会调用相应的信号处理程序对其进行处理。

图6-2 正常情况运行

图6-3 按下ctrl Z的情况

图6-4 按下ctrlC的情况

图6-5 乱按的情况
图6-2:正常情况
图6-3:,是在程序输出 2 条 info 之后按下 ctrl-z 的结果,当按下 ctrl-z 之后,shell 父进程收到 SIGSTP 信号,信号处理函数的逻辑是打印屏幕回显、将 hello 进程挂起,通过 ps 命令我们可以看出 hello 进程没有被回收,此时他的后台 job 号是 1,调用 fg 1 将其调到前台,此时 shell 程序首先打印 hello 的命令行命令, hello 继续运行打印剩下的 8 条 info,之后输入字串,程序结束,同时进程被回收。
图6-4:是在程序输出 3 条 info 之后按下 ctrl-c 的结果,当按下 ctrl-c 之 后,shell 父进程收到 SIGINT 信号,信号处理函数的逻辑是结束 hello,并回收 hello 进程。
图6-5:可见乱按并不影响正常运行。
6.7本章小结
本章介绍了程序在shell执行及进程的相关概念。程序在shell中执行是通过fork函数及execve创建新的进程并执行程序。进程拥有着与父进程相同却又独立的环境,与其他系统进并发执行,拥有各自的时间片,在内核的调度下有条不紊的执行着各自的指令。
并且介绍了在运行过程中的异常与信号处理。
(第6章1分)

第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:逻辑地址空间的格式为“段地址:偏移地址”,例如“23:8048000”,在实模式下可以转换为物理地址:逻辑地址CS:EA = 物理地址CS × 16 + EA。保护模式下以段描述符作为下标,通过在GDT/LDT表获得段地址,段地址加偏移地址得到线性地址。
线性地址:线性地址指虚拟地址到物理地址变换的中间层,是处理器可寻址的内存空间(称为线性地址空间)中的地址。程序代码会产生逻辑地址,或者说段中的偏移地址,加上相应段基址就成了一个线性地址。如果启用了分页机制,那么线性地址可以再经过变换产生物理地址。若是没有采用分页机制,那么线性地址就是物理地址。
虚拟地址:此处就是线性地址
物理地址:物理地址是用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。现代操作系统都提供了一种内存管理的抽像,即虚拟内存。进程使用虚拟内存中的地址,即虚拟地址,由操作系统协助相关硬件,把它“转换”成真正的物理地址。hello.s中使用的就是虚拟空间的虚拟地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
Intel处理器从逻辑地址到线性地址的变换通过段式管理,介绍段式管理就必须了解段寄存器的相关知识。段寄存器对应着内存不同的段,有栈段寄存器(SS)、数据段寄存器(DS)、代码段寄存器(CS)和辅助段寄存器(ES/GS/FS)。其大体对应关系如下图:
图7-1 段寄存器
段寄存器用于存放段选择符,通过段选择符可以得到对应段的首地址。段选择符分为三个部分,分别是索引、TI(决定使用全局描述符表还是局部描述符表)和RPL(CPU的当前特权级)。

图7-2 段选择符
Intel处理器在通过段式管理寻址时,首先通过段描述符得到段基址,然后与偏移量结合得到线性地址,从而得到了虚拟地址。至于偏移量,基址寄存器还是变址寄存器有不同的计算方法,后者需要经过乘比例因子等处理。
7.3 Hello的线性地址到物理地址的变换-页式管理
虚拟内存系统将虚拟内存分割称为虚拟页,物理内存分割称为物理页。

图7-3 虚拟页与物理页
页表将虚拟页映射到物理页,其每一项称为页表条目,由有效位和一个n位的地址字段组成。如果设置有效位说明该页已缓存,否则未缓存,地址字段不为空时指向虚拟页在磁盘上的起始地址。
虚拟地址到物理地址的翻译时MMU通过虚拟地址索引到对应的页表条目,如果已缓存命中,否则不命中称为缺页。发生缺页时,MMU会选择一个牺牲页,将之前缺页的虚拟内存对应的数据复制到它的位置,并更新页表,然后重新触发虚拟地址翻译。
通过页表,MMU可实现从虚拟地址到物理地址的映射。

图7-4 使用页表的地址翻译
CPU中的页表基址寄存器指向当前页表,n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移(VPO)和一个n- p位的虚拟页号(VPN)。
MMU利用VPN选择适当的PTE,然后将物理的PPN虚拟地址中的VPO联起来,得到物理地址。
如果缺页需要操作系统内核与硬件合作完成。
图7-5 页面命中和缺页的操作图
7.4 TLB与四级页表支持下的VA到PA的变换
在 Intel Core i7 环境下研究 VA 到 PA 的地址翻译问题。前提如下: 虚拟地址空间 48 位,物理地址空间 52 位,页表大小 4KB,4 级页表。TLB 4 路 16 组相联。CR3 指向第一级页表的起始位置(上下文一部分)。 解析前提条件:由一个页表大小 4KB,一个 PTE 条目 8B,共 512 个条目,使 用 9 位二进制索引,一共 4 个页表共使用 36 位二进制索引,所以 VPN 共 36 位, 因为 VA 48 位,所以 VPO 12 位;因为 TLB 共 16 组,所以 TLBI 需 4 位,因为 VPN 36 位,所以 TLBT 32 位。 CPU 产生虚拟地址 VA,VA 传送给 MMU,MMU 使用前 36 位 VPN 作为 TLBT(前 32 位)+TLBI(后 4 位)向 TLB 中匹配,如果命中,则得到 PPN (40bit)与 VPO(12bit)组合成 PA(52bit)。 如果 TLB 中没有命中,MMU 向页表中查询,CR3 确定第一级页表的起始地 址,VPN1(9bit)确定在第一级页表中的偏移量,查询出 PTE,如果在物理内存 中且权限符合,确定第二级页表的起始地址,以此类推,最终在第四级页表中查 询到 PPN,与 VPO 组合成 PA,并且向 TLB 中添加条目。 如果查询 PTE 的时候发现不在物理内存中,则引发缺页故障。如果发现权限 不够,则引发段错误。

图7-6 Core i7页表翻译
7.5 三级Cache支持下的物理内存访问
前提:只讨论 L1 Cache 的寻址细节,L2 与 L3Cache 原理相同。L1 Cache 是 8 路 64 组相联。块大小为 64B。 解析前提条件:因为共 64 组,所以需要 6bit CI 进行组寻址,因为共有 8 路, 因为块大小为 64B 所以需要 6bit CO 表示数据偏移位置,因为 VA 共 52bit,所以 CT 共 40bit。 在上一步中我们已经获得了物理地址 VA,如图,使用 CI(后六位再后六 位)进行组索引,每组 8 路,对 8 路的块分别匹配 CT(前 40 位)如果匹配成功 且块的 valid 标志位为 1,则命中(hit),根据数据偏移量 CO(后六位)取出数 据返回。 如果没有匹配成功或者匹配成功但是标志位是 1,则不命中(miss),向下一 级缓存中查询数据(L2 Cache->L3 Cache->主存)。查询到数据之后,一种简单的 放置策略如下:如果映射到的组内有空闲块,则直接放置,否则组内都是有效块, 产生冲突(evict),则采用最近最少使用策略 LFU 进行替换。

图7-7 三级Cache支持下的物理内存访问
7.6 hello进程fork时的内存映射
虚拟内存和内存映射解释了fork函数如何为每个新进程提供私有的虚拟地址空间。Fork函数为新进程创建虚拟内存。创建当前进程的的mm_struct, vm_area_struct和页表的原样副本,两个进程中的每个页面都标记为只读,两个进程中的每个区域结构(vm_area_struct)都标记为私有的写时复制(COW)。在新进程中返回时,新进程拥有与调用fork进程相同的虚拟内存,随后的写操作通过写时复制机制创建新页面。
7.7 hello进程execve时的内存映射
execve函数在shell中加载并运行包含在可执行文件hello中的程序,用hello程序有效地替代了当前程序。加载hello的过程主要步骤如下:
首先删除已存在的用户区域,也就是将shell与hello都有的区域结构删除。然后映射私有区域,即为新程序的代码、数据、bss和栈区域创建新的区域结构,均为私有的、写时复制的。下一步是映射共享区域,将一些动态链接库映射到hello的虚拟地址空间,最后设置程序计数器,使之指向hello程序的代码入口。
经过这个内存映射的过程,在下一次调度hello进程时,就能够从hello的入口点开始执行了。
7.8 缺页故障与缺页中断处理
缺页故障是一种常见的故障,当指令引用一个虚拟地址,在 MMU 中查找页表 时发现与该地址相对应的物理地址不在内存中,因此必须从磁盘中取出的时候就 会发生故障。其处理流程遵循图 7-8所示的故障处理流程。

图7-8 故障处理流程
缺页中断处理:缺页处理程序是系统内核中的代码,选择一个牺牲页面,如果 这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺 页处理程序返回时,CPU 重新启动引起缺页的指令,这条指令再次发送 VA 到 MMU,这次 MMU 就能正常翻译 VA 了。
7.9动态存储分配管理
动态存储分配管理由动态内存分配器完成。动态内存分配器维护着一个进程的虚拟内存区域,称为堆。堆是一个请求二进制零的区域,它紧接在未初始化的数据区后开始,并向上生长(向更高的地址)。分配器将堆视为一组不同大小的块的集合来维护。
每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可以用来分配。空闲块保持空闲,直到它显示地被应用程序所分配。
一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
动态内存分配器从堆中获得空间,将对应的块标记为已分配,回收时将堆标记为未分配。而分配和回收的过程中,往往涉及到分割、合并等操作。
动态内存分配器的目标是在对齐块的基础上,尽可能地提高吞吐率及空间占用率,即减少因为内存分配造成的碎片。其实现常见的数据结构有隐式空闲链表、显式空闲链表、分离空闲链表,常见的放置策略有首次适配、下一次适配和最佳适配。
为了更好的介绍动态存储分配的实现思想,以隐式空闲分配器的实现原理为例进行介绍:
图7-9 隐式空闲链表堆块结构
隐式空闲链表分配器的实现涉及到特殊的数据结构。其所使用的堆块是由一个子的头部、有效载荷,以及可能的一些额外的填充组成的。头部含有块的大小以及是否分配的信息。有效载荷用来存储数据,而填充块则是用来对付外部碎片以及对齐要求。
基于这样的基本单元,便可以组成隐式空闲链表。

图7-10 隐式空闲链表结构
通过头部记录的堆块大小,可以得到下一个堆块的大小,从而使堆块隐含地连接着,从而分配器可以遍历整个空闲块的集合。在链表的尾部有一个设置了分配位但大小为零的终止头部,用来标记结束块。
当请求一个k字节的块时,分配器搜索空闲链表,查找足够大的空闲块,其搜索策略主要有首次适配、下一次适配、最佳适配三种。
一旦找到空闲块,如果大小匹配的不是太好,分配器通常会将空闲块分割,剩下的部分形成一个新的空闲块。如果无法搜索到足够空间的空闲块,分配器则会通过调用sbrk函数向内核请求额外的堆内存。
当分配器释放已分配块后,会将释放的堆块自动与周围的空闲块合并,从而提高空间利用率。为了实现合并并保证吞吐率,往往需要在堆块中加入脚部进行带边界标记的合并。
7.10本章小结
本章主要介绍了 hello 的存储器地址空间、intel 的段式管理、hello 的页式管理, 以 intel Core7 在指定环境下介绍了 VA 到 PA 的变换、物理内存访问,还介绍了 hello 进程 fork 时的内存映射、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理。
(第7章 2分)

第8章 hello的IO管理
8.1 Linux的IO设备管理方法
一个Linux文件就是一个m个字节的序列,所有的I/O设备都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这个设备映射为文件的方式,允许Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使得输入和输出都能以一种统一且一致的方式的来执行。
一个应用程序通过要求内核打开相应的文件来宣告它想访问一个I/O设备。内核返回一个小的非负整数,叫做描述符,而文件的相关信息由内核记录,应用程序只需要记录这个描述符。
Linux shell创建的每个进程开始时都包含标准输入、标准输出、标准错误三个文件,供其执行过程中使用。
对于每个打开的文件,内核保持着一个文件位置k,初始为0,即从文件开头起始的字节偏移量,应用程序能够通过执行seek操作来显式的改变其值。
至于读操作,就是从文件复制n个字节到内存,并将文件位置k增加为k + n。当k大于等于文件大小时,触发EOF条件,即读到文件的尾部。
最后,在结束对文件的访问后,会通过内核关闭这个文件,内核将释放打开这个文件时创建的数据结构,并将描述符恢复到可用的描述符池中。

8.2 简述Unix IO接口及其函数
Linux以文件的方式对I/O设备进行读写,将设备均映射为文件。对文件的操作,内核提供了一种简单、低级的应用接口,即Unix I/O接口。

Unix I/O接口提供了以下函数供应用程序调用:

打开文件:int open(char *filename, int flags, mode_t mode);

关闭文件:int close(int fd);

读文件:ssize_t read(int fd, void *buf, size_t n);

写文件:ssize_t write(int fd, const void *buf, size_t n);

8.3 printf的实现分析
1.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;
}
2.首先 arg 获得第二个不定长参数,即输出的时候格式化串对应的值。 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 结合参数 args 生成格式化之后的字符串,并 返回字串的长度。 在 printf 中调用系统函数 write(buf,i)将长度为 i 的 buf 输出。在Linux下,write函数的第一个参数为fd,也就是描述符,而1代表的就是标准输出。查看write函数的汇编实现可以发现,它首先给寄存器传递了几个参数,然后调用syscall结束。write通过执行syscall指令实现了对系统服务的调用,从而使内核执行打印操作。
syscall 将字符串中的字节“Hello 1170300921 wangjiangrui”从寄存器中通过总线复 计算机系统课程报告制到显卡的显存中,显存中存储的是字符的 ASCII 码。 字符显示驱动子程序将通过 ASCII 码在字模库中找到点阵信息将点阵信息存 储到 vram 中。 显示芯片会按照一定的刷新频率逐行读取 vram,并通过信号线向液晶显示器传输每一个点(RGB 分量)。 于是我们的打印字符串“Hello 1170300921 wangjiangrui”就显示在了屏幕上。

8.4 getchar的实现分析
getchar的实现如下:
Int getchar(void)
{
Char c;
Return (read(0,&c,1)==1)?(unsigned char)c:EOF
}
异步异常-键盘中断的处理:当用户按键时,键盘接口会得到一个代表该按键 的键盘扫描码,同时产生一个中断请求,中断请求抢占当前进程运行键盘中断子 程序,键盘中断子程序先从键盘接口取得该按键的扫描码,然后将该按键扫描码 转换成 ASCII 码,保存到系统的键盘缓冲区之中。
getchar 函数落实到底层调用了系统函数 read,通过系统调用 read 读取存储在键盘缓冲区中的 ASCII 码直到读到回车符然后返回整个字串,getchar 进行封装, 大体逻辑是读取字符串的第一个字符然后返回。
8.5本章小结
本章主要介绍了 Linux 的 IO 设备管理方法、Unix IO 接口及其函数,分析了 printf 函数和 getchar 函数。
(第8章1分)
结论
用计算机系统的语言,逐条总结hello所经历的过程:
hello.c通过键盘鼠标等I/O设备输入计算机,并存储在内存中。然后预处理器将hello.c预处理成为文本文件hello.i。接着编译器将hello.i翻译成汇编语言文件hello.s。汇编器将hello.s汇编成可重定位二进制代码hello.o。链接器将外部文件和hello.o连接起来形成可执行二进制文件hello.out。shell通过fork和execve创建进程,然后把hello加载到其中。shell创建新的内存区域,并加载代码、数据和堆栈。hello在执行的过程中遇到异常,会接受shell的信号完成处理。hello在执行的过程中需要使用内存,那么就通过CPU和虚拟空间进行地址访问。Hello执行结束后,shell回收其僵尸进程,从系统中消失。

你对计算机系统的设计与实现的深切感悟,你的创新理念,如新的设计与实现方法:
计算机系统过于虚拟,虚拟代表高级,设计过程虽然复杂,但是却无一不体现计算机的思想和高级之处,凡世界之物,均在系统之内。高级语言表达了汇编语言,汇编语言表达了机器语言,并且随着技术的发展,硬件性能的不断提升,计算机系统还会向着综合与精分两方面不断发展,所以为了计算机的伟大发展,让我们一起加油吧。
(结论0分,缺失 -1分,根据内容酌情加分)

附件
Hello.c c程序代码
Hello.i 预处理之后的文本文件
Hello.s 编译之后的汇编文件
Hello.o 汇编之后的可重定位目标程序
Hello 链接之后的可执行目标文件
Helloo.objdmp hello.o的反汇编代码
Helloo.elf hello.o的ELF格式
Hello.objdmp hello的反汇编代码
Hello.elf hello的ELF格式
(附件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.
[7] 兰德尔E.布莱恩特 大卫R.奥哈拉伦. 深入理解计算机系统(第3版).
机械工业出版社.
[8] https://blog.csdn.net
(参考文献0分,缺失 -1分)

程序人生-hello`s P2P相关推荐

  1. 哈工大计算机系统2022大作业:程序人生-Hello‘s P2P

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算机类 学    号 120L022115 班    级 2003007 学       生 王炳轩 指 导 ...

  2. 哈工大计算机系统大作业:程序人生-Hello’s P2P

    题     目 程序人生-Hello's P2P 专       业 计算学部 学   号 120L022028 班   级 2003007 学       生 杨建中 指 导 教 师 吴锐 计算机科 ...

  3. 2022计算机系统大作业——程序人生-Hello’s P2P

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算机 学    号 120L021716 班    级 2003005 学       生 蔡泽栋 指 导 ...

  4. 哈工大计算机系统大作业——程序人生-Hello’s P2P

    计算机系统 大作业 题          目程序人生-Hello's P2P 专          业 计算机科学与技术 学       号120L022401 班          级 200300 ...

  5. 哈工大CSAPP大作业:程序人生-Hello’s P2P

    ​     计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算学部 学    号 120L021818 班    级 2003006 学       生 秦 ...

  6. HIT CS:APP 计算机系统大作业 《程序人生-Hello’s P2P》

    HIT CS:APP 计算机系统大作业 程序人生-Hello's P2P Hello的自白 我是Hello,我是每一个程序猿¤的初恋(羞羞--) l却在短短几分钟后惨遭每个菜鸟的无情抛弃(呜呜--), ...

  7. 【计算机系统】ICS大作业论文-程序人生-Hello’s P2P

    计算机系统 大作业 题 目 程序人生-Hello's P2P 专 业 计算机科学与技术专业 学 号 200111026 班 级 20级计算机9班 学 生 陈弘毅 指 导 教 师 郑贵滨 计算机科学与技 ...

  8. HIT-ICS大作业-程序人生Hello‘s P2P

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算机类 学    号 120L021311 班    级 2003011 学       生 郭俊诚 指 导 ...

  9. HIT-ICS2022大作业(程序人生-Hello’s P2P)

    计算机系统 大作业 题     目 程序人生-Hello's P2P 专       业 计算机科学与技术 学    号 班    级 学       生 指 导 教 师 计算机科学与技术学院 202 ...

最新文章

  1. golang interface 转 int string slice struct 类型
  2. python stm32-STM32 上面跑Python
  3. vue手机端回退_华为官方教程:以 P40 为例,鸿蒙 OS 2.0 Beta 版本回退到 EMUI 11 稳定版...
  4. 小米android11新特性,小米已推送安卓11测试版 目前支持三款机型
  5. java io系统_java中的io系统详解
  6. 在VS Code中直接调试Web程序,是怎样一种体验?
  7. 架构师成长之路:如何保证消息队列的高可用
  8. 数据结构:从插入排序到希尔排序
  9. Dart的数据库操作
  10. ❤️《集成SSM框架—图书系统》Mybatis+Spring+SpirngMVC
  11. FCKeditor在线编辑器的使用
  12. Wireshark实战分析值ICMP协议(一)
  13. php安装包 64位,PHP5.2集成环境安装包下载
  14. 0 、 '0' 、 0 、 ’\0’ 区别
  15. A Survey on Conversational Recommender Systems(2021)阅读笔记
  16. linux常用软件收集
  17. tp路由服务器无响应,路由器无法PPPOE链接上网老显示服务器无响应
  18. 树模型知识点(1)——决策树
  19. 为什么HashMap的key允许空值,而Hashtable却不允许
  20. 在线画图软件draw.io

热门文章

  1. [usaco6.1.1Postal Vans]
  2. Unity实战篇:实现虚拟摇杆控制人物(Easy Touch 5.X插件)
  3. 系统错误null是什么意思_为什么NULL是错误的?
  4. OpenFlow1.0协议解析
  5. 线段树 - 敌兵布阵
  6. python程序基础网课答案_知到Python程序设计基础网课答案
  7. 网络的形成-从原始部落到现代化世界
  8. C语言填空答题考试系统
  9. 工具篇——MoneyFormatUtil(用于将人民币小写金额转换成大写金额)
  10. springboot实现的工厂模式