【2024HIT-CS大作业】程序人生-Hello’s P2P

打印 上一主题 下一主题

主题 1075|帖子 1075|积分 3229

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

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

x

摘  要

本文以展现Hello复杂一生作为出发点,结合在《深入理解计算机系统》及其课程中所学习到的知识,对程序在Linux系统下的整个生命周期进行深度剖析。从hello.c开始渐渐探究在计算机系统下程序的预处置惩罚、编译、汇编、链接、历程管理、存储管理、I\O管理等方面,将计算机系统整个体系串联到一起,真正将计算机知识形成体系,融会贯通,进而了解计算机系统的奥妙与伟大。

关键词:程序人生;计算机系统;Linux ;汇编         
目  录


第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.3.1 汇编文件内容解析

3.3.2整型数据类型

3.3.3字符串数据类型

3.3.4数组数据类型及操作

3.3.5赋值操作

3.3.6类型转换与sizeof

3.3.7算术操作

3.3.8关系操作

3.3.9控制转移

3.3.10函数操作

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 本章小结

6hello历程管理

6.1 历程的概念与作用

6.2 简述壳Shell-bash的作用与处置惩罚流程

6.3 Hello的fork历程创建过程

6.4 Hello的execve过程

6.5 Hello的历程实行

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

6.7本章小结

7hello的存储管理

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动态存储分配管理

7.10本章小结

8hello的IO管理

8.1 Linux的IO装备管理方法

8.2 简述Unix IO接口及其函数

8.3 printf的实现分析

8.4 getchar的实现分析

8.5本章小结

结论

附件

参考文献



第1章 概述

1.1 Hello简介

Hello.c是每一个程序员接触的第一个程序,简短的代码背后是CS的方方面面。Hello程序的生命周期是从一个高级C语言程序开始的,因为这种形式能够被人读懂。
P2P(From Program to Process):hello.c在计算机中须要颠末预处置惩罚阶段、编译阶段、汇编阶段、链接阶段才可以生成一个可实行文件。在源程序文件翻译成可实行程序后,Linux系统通过shell运行hello。Shell在运行hello时通过系统调用(system call)来实行哀求。操作系统保存shell历程的上下文,并使用fork函数创建一个新的hello历程及其上下文,至此完成由程序到历程的转变。
020(From Zero-0 to Zero-0):初始时内存中没有hello文件的内容,shell调用了execve函数后系统吧hello文件载入内存并实行程序。运行后将hello历程回收,内核中相关数据也被删除,至此hello完成从0到0的过程。
1.2 情况与工具

硬件情况:X64 CPU;3.20 GHz;16G RAM;512G SSD Disk;
软件情况:Win 11 22H2;Ubuntu 20.04.2 LTS;WSL2;VMware workstation
开发工具:Visual Studio 2022 64位;GDB/OBJDUMP;DDD/EDB等;
1.3 中间结果

hello.c : 源文件
hello.i : 颠末预处置惩罚生成的文件
hello.s : 颠末编译生成的文件
hello.o : 颠末汇编产生的可重定向文件
hello_o.elf : hello的ELF格式文件
hello_o.s: 使用objdump查看hello.o的反汇编代码
hello : 终极生成的可执举动文件
hello_elf:hello程序的elf格式文件
Hello_asm.s:hello反汇编生成的文件
1.4 本章小结

本章简要先容了hello程序的P2P,020的过程,本次实验的情况工具,以及实验过程中,生成的中间结果文件。


第2章 预处置惩罚

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

预处置惩罚器(cpp)根据以#开头的下令,修改原始的C程序。比如hello.c中第一行的#include <stdio.h>下令告诉预处置惩罚器读取系统头文件stdio.h中的内容,并把它直接插入到程序文本中。结果就得到了另一个C程序,通常是以.i作为文件的扩展名。
预处置惩罚只是进行简单的文本分割、插入和更换,方便后续的处置惩罚,而并未对源代码内容进行任何直接的解析。
2.2在Ubuntu下预处置惩罚的下令

在shell输入指令:gcc -E hello.c -o hello.i 或 cpp hello.c > hello.i

图2-1

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

终端输入指令vim hello.i

图2-2

由截图2-2可以看到hello.i已经扩展到了3107行


图2-3

由图2-3可以看出hello.i文件中3079-3092行是main函数,而在3079行之前的内容是对头文件 <stdio.h> <unistd.h> <stdlib.h>的递归展开,内容包括函数声明、结构体定义、变量定义等。

2.4 本章小结

本章针对程序翻译过程中的第一个阶段——预处置惩罚进行展开探究。主要先容了预处置惩罚的概念与作用、在Ubuntu下预处置惩罚的下令以及Hello程序颠末预处置惩罚后的简要分析。

第3章 编译

3.1 编译的概念与作用

编译器(ccl)将文本文件hello.i翻译成汇编语言文件hello.s,它包罗一个汇编语言程序。该程序包罗函数main的定义,定义中每条语句都以一种文本格式形貌一条低级机器语言指令,方便后续转化成二进制机器码。
3.2 在Ubuntu下编译的下令

在终端输入指令:gcc -S hello.i -o hello.s
生成hello.s文件:

图3-1


3.3 Hello的编译结果解析

3.3.1 汇编文件内容解析

伪指令
含义
.file
声明源文件
.text
代码段
.section.rodata
只读数据段
.global
声明一个全局变量
.string
字符串
.long
长整型
.type
用来指定函数类型大概对象类型
.size
设定指定符号的巨细
.align
数据对齐

3.3.2整型数据类型


图3-2

由图3-2可以看出main函数中有两个整型数据,一个是i,一个是argc。起首分析argc。根据x86-64的寄存器使用,argc作为第一个参数传入main应该存在%ebi中,通过查看hello.s文件可以看出%ebi被存在-20(%rbp)中,如图3-3所示:

图3-3

在源程序中argc作为循环判定条件,在汇编文件中也可以清楚地看到-20(%rbp)与5进行比力,用于条件跳转。

再来分析i,i起首以-4(%rbp)出现在L2而后跳转到L3与9进行比力,若即是9则跳出循环,作为循环的出口,L4中最后一举动逐次加1的指令。如下图所示:


图3-4


图3-5


图3-6

其他整型数据的出现都是以立即数的形式出现的。

3.3.3字符串数据类型

观察hello.s不难发现以.string开头的两个字符串数据,它们被存在rodata中作为只读数据,这两个字符串分别在L1和L4中各被调用了一次:

图3-7



图3-8

这两次调用对应的是程序中图3-9中的内容:

图3-9

3.3.4数组数据类型及操作

在程序中传入main函数的数组是char argv[],编译处置惩罚会将它存储到栈中。根据x86-64使用寄存器保存参数的规则,此时argv应该保存在%rsi中,实行时展会向下生长32字节,之后将数组压栈,如图3-10所示。

图3-10

数组的起始地址为 -32(%rbp),在L4中调用数组时直接从栈中读出。由addq  $8,%rax 的操作可以看出argv的数组元素为八个字节,此处分别调用了argv[1]、argv[2]、argv[3]用作printf函数的参数。然后取argv[4]中的元素,并将其取到%rax中,并放入%rdi中作为参数,调用atoi函数。并将返回结果%eax放入%edi中,调用sleep函数。如图3-11所示:


图3-11

3.3.5赋值操作

在源代码中进行赋值操作的语句是for(i=0;i<10;i++),根据3.3.2整型数据的分析在L2中-4(%rbp)被赋以0值。

图3-12

3.3.6类型转换与sizeof

在hello中未涉及到。
3.3.7算术操作

源代码中涉及到算术操作的语句是循环判定条件中的i++,根据上文3.3.2和3.3.5中内容可以看到hello.s在L4中进行对-4(%rbp)逐次加一的举动。事实上,算术操作不仅只在hello.c中出现算术操作的地方存在,比如在栈的存储过程中,栈指针的变更也依赖于算术操作,如图3-13所示。



图3-13


3.3.8关系操作

源代码总涉及到关系操作的语句是argc!=5以及i<10,如下图3-14所示:

图3-14

在hello.s中表现为两个cmpl语句:


图3-15

3.3.9控制转移

源代码中涉及到控制转移的语句是两个条件判定语句if(argc!=5)和for(i=0;i<10;i++)。
对argc的判定如3-16所示,若-20(%rbp)不即是5则跳转到L2.在原程序里表现为不进入if语句,如果不相称,不跳转继续实行,表现为进入if语句。

图3-16

对i的判定如图3-17,若-4(%rbp)小于即是9时跳转回L4,继续进行循环体内的操作。

图3-17

3.3.10函数操作

在源代码hello.c中一共存在7出函数调用,分别是:printf(),exit(),atoi(),sleep(),getchar(),main().

  • printf()&exit()

图3-18

在LFB6中当argc不即是5时,函数会调用puts函数打印出"用法: Hello 学号 姓名 手机号 秒数!"这句话,然后将%edi赋值为1作为exit()函数的参数。至此完成了两次函数调用。

  • Printf(),atoi(),sleep()

图3-19

在L4中,起首调用了argv[1],argv[2],argv[3]作为printf函数的参数,当for循环中i小于10时跳转到L4打印出“hello argv[1] argv[2] argv[3]”语句。然后调用argv[4]作为atoi的参数并将结果存在%edi中作为sleep函数的参数,调用sleep函数。至此完成了三次函数调用。

  • Getchar()
在L3中如果i大于即是10了则会跳出循环体,调用getchar()函数。

图3-20

留意此时存在对main函数的调用将%eax赋值为0,作为main函数的返回值。通过leave和ret指令竣事程序。至此程序总计完成七次函数调用。

3.4 本章小结

本章从程序的编译过程出发,就编译的概念及作用进行简单分析,着重根据hello.s文件从数据、赋值操作、类型转换、算术操作、逻辑操作、关系操作、数组指针结构操作、控制转移以及函数操作多角度深刻剖析汇编语言的原理与调用过程,对于笔者及读者进一步理解汇编语言有很大帮助。

第4章 汇编


4.1 汇编的概念与作用


汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中。这样的过程称之为汇编。将汇编文本程序翻译成机器语言指令并打包成可重定位目标程序。可重定位目标程序文件是一个二进制文件,它包罗的字节是函数main的指令编码。
4.2 在Ubuntu下汇编的下令

以下格式自行编排,编辑时删除
应截图,展示汇编过程!
在Ubuntu下汇编可使用指令:as hello.s -o hello.o 或gcc -c hello.s -o hello.o

图4-1

4.3 可重定位目标elf格式

使用下令 readelf -a hello.o 可以重定位目标文件hello.o的ELF格式。ELF文件结构如下图所示

图4-2

1.ELF头以一个16字节的magic数为开始,这个序列形貌了系统的字的巨细及字节次序剩下的的部分包括elf头的巨细、目标文件的类型、机器类型、版本、节头部表的文件偏移以及节头部表中条目的巨细和数量。

图4-3



  • 节头部表:包括节名称,节的类型,节的属性(读写权限),节在ELF文件中所占的长度以及节的对齐方式和偏移量。我们可以使用终端指令readelf -S hello.o来查看节头表。

图4-4


  • 重定位条目:包罗了链接时重定位所需的全部信息:须要被重定位的代码在其段中的偏移、该段代码所对应的符号在符号表中的索引以及重定位类型、重定位时被使用到的加数。如图4-7所示,为hello.o对应的重定位表。
.rela.text节是一个.text节中位置的列表。当链接器把这个目标文件和其他文件组适时,须要修改这些位置。一样平常而言,任何调用外部函数大概应用全局变量的指令都须要修改。

图4-5


  • 符号表:存放了程序中定义和引用的函数和全局变量。在hello.c中我们没有定义全局变量,所以符号表中没有他们的身影,只能看到函数。

图4-6

4.4 Hello.o的结果解析

以下格式自行编排,编辑时删除
在Ubuntu下对hello.o进行反汇编,在shell输入指令,将反汇编的代码写入hello_o.s中:objdump -d -r hello.o > hello_o.s  
将hello.s与hello_o.s放在一起进行比力,发现两者大要上基本一致,只在一些小部分上有区别,详细表现在以下几方面:

  • 操作数:在hello.s中操作数为十进制,在这里为十六进制。

图4-7


  • 分支转移地址:在hello.s中分支转移通过L2,L3实现跳转,而在此处分支转移通过计算地址跳转。

图4-8


  • 函数调用:在hello.s文件中,函数调用call只须要加函数名称,在这里call则是使用了当前指令的下一个字节。原因是因为该函数迟绑定,该函数为共享库中函数,只有运行时,动态链接器作用后才能确定相应的PLT条目地址。

图4-9


  • 全局变量:hello.s上是通过".LC1(%rip)"的形式访问,在这里是以"0x0(%rip)"的形式访问,添加了重定位条目。

图4-10

完整对比如图4-11所示:

图4-11

总结来说,机器码、指令和汇编语言之间的关系可以理解为一种条理结构:机器码位于最底层,是计算机硬件直接实行的二进制代码;指令是机器码的集合,用于控制计算机实行一系列操作;汇编语言位于中间层,作为机器码和高级语言之间的桥梁,提供了一种更接近硬件的低级编程方式。
4.5 本章小结

本章对汇编做了详尽的先容,详细先容了汇编的概念、作用,实践了在Ubuntu下汇编的下令,以及详细解读了可重定位目录elf目标格式,并对hello.o的结果作出了详细的分析,将hello.s和hello.o的反汇编代码对比。
通过理解这些概念及其之间的关系,我们可以更好地理解计算机程序的实行过程和底层机制。这对于从事底层系统开发、操作系统内核编程、嵌入式系统开发等范畴的程序员来说尤为重要。同时,对于学习计算机体系结构、编译原理等课程的读者来说,把握这些概念也是必不可少的。

5链接


5.1 链接的概念与作用

链接是将各种代码和数据片断网络并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并实行。链接可以实行于编译时,也就是在源代码被翻译成机器代码时,也可以实行于加载时,也就是在程序被加载器加载到内存并实行时;乃至实行于运行时,也就是由应用程序来实行。在现代系统中,链接是由叫做毗连器的程序主动实行的。
链接使分离编译成为可能。我们不用将一个大型的应用程序构造为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们该边这些模块中的一个时,只需简单地重新编译它,并重新链策应用,而不必重新编译其他文件。
5.2 在Ubuntu下链接的下令

在Ubuntu下链接,使用ld的链接下令应为:
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

也可以通过gcc hello.o -o hello实现链接,在这里我们使用ld链接。

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

起首生成hello的ELF文件,在shell中输入:readelf -a hello > hello_elf

  • hello的ELF头:以16字节的序列Magic开始,这个序列形貌了生成该文件的系统的字的巨细和字节次序。ELF头剩下的部分包罗帮助毗连器语法分析和解释目标文件的信息。此中包罗ELF头的巨细、目标文件的类型(如可重定位、可实行大概共享的)、机器类型(如x86-64)、节头部表的文件偏移,以及节头部表中条目的巨细和数量。不同节的位置和巨细是由节头部表形貌的,此中目标文件中每个节都有一个固定巨细的条目,与链接前的ELF header相比,除了系统决定的基本信息稳固,section header和paogram header都增长,并且增长了入口处的地址。如图5-2所示:

图5-2


  • 节头:节头表对hello中全部节的信息进行了声明,此中包括巨细Size以及在程序中的偏移量Offset,因此根据Section Headers中的信息,我们可以用Hexedit定位到各个节地点的空间。Address为程序被载入到捏造地址的起始空间。

图5-3


  • 程序头部表:ELF文件头结构就像是一个总览图,形貌了整个文件的布局情况,因此在ELF文件头结构答应的数值范围,整个文件的巨细是可以动态增减的,告诉系统怎样创建历程映像。如图5-4所示

图5-4


  • 动态偏移表:若目标文件参与动态链接,则其程序头表将包罗一个类型为PT_DYNAMIC的元素,此段包罗.dynamic节。特殊符号_DYNAMIC用于标志包罗以下结构的数组的节,如图5-5所示。

图5-5


  • 重定义节:.rela.text,一个.text节中位置的列表,包罗.text节中须要进行重定位的信息,当链接器把这个目标文件和其他文件组适时,须要修改这些位置。如图5-6所示,分别形貌了main、puts、printf、getchar、atoi、exit、sleep函数的重定位声明。

图5-6


  • 动态符号表用来保存于动态链接相关的导入导出符号,不包括模块内部的符号。


图5-7


  • Symbol table:目标文件的符号表包罗定位和重定位程序的符号定义和符号引用所需的信息。符号表索引是此数组的下标。索引0指定表中第一项用作未定义的符号索引。


图5-8

5.4 hello的捏造地址空间

使用edb加载hello,在shell中输入指令:edb 启动程序并打开hello ,如图5-9所示:

图5-9

观察edb的Data Dump窗口。窗口显示捏造地址由0x401000开始,到0x401ff0竣事。对应5.3中的节头表的声明,根据截图5-3可得起始位置为0x401000。Data Dump窗口如图5-10所示:


图5-10

再观察edb的Symbols窗口,如图5-11所示,与图5-3中的各节所示一致。


图5-11

5.5 链接的重定位过程分析

使用指令objdump -d -r hello > hello_asm.s对hello进行反汇编,生成.s文件方便查看对比。

图5-12

分析两者的区别:


    • hello的反汇编代码中,比hello.o的反汇编代码中多了.init节,.plt节,.fini节,.plt.sec等。


图5-13



    • hello的反汇编代码中增长了外部链接的共享库函数。例如在hello的反汇编代码中可以看到puts@plt等,以atoi函数为例如下图所示:



图5-14 hello


图5-15 hello.o



    • hello的反汇编代码相比hello.o的反汇编代码,跳转地址修改为了捏造内存地址。
    • hello中节的起始位置修改为了捏造地址,hello.o反汇编代码中为相对偏移地址。


图5-16

结合hello.o的重定位项目,可以得出在重定位过程中,链接器将符号解析完成后,就将代码中的全部符号引用都与一个符号定义关联。这样链接器就可以按照各个内容的详细巨细来进行重定位,合并输入模块,并为每个符号分配运行时的地址。
5.6 hello的实行流程

函数
地址
<_init>
0000000000401000
<puts@plt>
0000000000401090
<printf@plt>
00000000004010a0
<getchar@plt>
00000000004010b0
<atoi@plt>
00000000004010c0
<exit@plt>
00000000004010d0
<sleep@plt>
00000000004010e0
<_start>
00000000004010f0
<_dl_relocate_static_pie>
0000000000401120
<main>
0000000000401125
<_fini>
00000000004011c8
使用edb程序进行查看,loaded symbol窗口所示即为hello的实行流程:

5.7 Hello的动态链接分析

   (以下格式自行编排,编辑时删除
程序调用一个由共享库定义的函数时,编译器无法预测这个函数运行时的地址,因为定义它的共享模块在运行时可以加载到任何位置。GUN编译系统使用一种很有趣的技术来办理这个问题,称为延迟绑定——将过程地址的绑定推迟到第一次调用该过程时。延迟绑定通过全局偏移量表GOT和过程链接表PLT的协作来解析函数的地址。如果一个目标模块调用定义在共享库中的任何函数,那么他就有自己的GOT和PLT。GOT是数据段的一部分,而PLT是代码段的一部分。

在加载时,动态链接器会重定位GOT中的每个条目,使它包罗正确的绝对地址,而PLT中的每个函数负责调用不同函数。那么,通过观察edb,便可发现dl_init后.got.plt节发生的变化。
查看elf文件得到.got.plt的起始位置是0x404000,竣事于0x404048。

在进举措态链接前:

进举措态链接之后:

留意到发生改变的部位对应GOT[1]和GOT[2]的位置。此中,这时GOT[1]包罗动态链接器在解析函数地址时使用的信息,而GOT[2]是动态链接器ld-linux.so模块中的入口点。
5.8 本章小结

本章详细讨论了链接的概念与作用、在Ubuntu下链接的下令,以及可实行目标文件hello的格式,详细分析可实行目标文件和可重定位目标文件的区别。详细解释了hello捏造地址空间。细致分析了链接的重定位过程。模拟hello的实行流程,并深入探讨了hello的动态链接。

6hello历程管理


6.1 历程的概念与作用

历程的经典定义就是一个实行中程序的实例。系统中的每个程序都运行在某个历程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、情况变量以及打开文件形貌符的集合。
在现代系统上运行一个程序时,历程为用户提供一个假象:就似乎我们的程序似乎是系统中当前运行的唯一程序一样,我们的程序似乎是独占的使用处置惩罚器和内存,处置惩罚器似乎是无间断的实行我们程序中的指令,我们程序中的代码和数据似乎是系统内存中唯一的对象。每次用户通过向shell 输入一个可实行目标文件的名字,运行程序时, shell 就会创建一个新的历程,然后在这个新历程的上下文中运行这个可实行目标文件。应用程序也能够创建新历程,并且在这个新历程的上下文中运行它们自己的代码或其他应用程序。
历程提供给应用程序两个关键抽象:一个独立的逻辑控制流;一个私有的地址空间。
6.2 简述壳Shell-bash的作用与处置惩罚流程

Shell-bash的作用:Shell是一个下令行解释器,它为用户提供了一个向Linux内核发送哀求以便运行程序的界面系统级程序,用户可以用Shell来启动、挂起、停止乃至编写一些程序。Shell还是一个功能相称强大的编程语言,易编写,易调试,机动性较强。Shell是解释实行的脚本语言,在Shell中可以直接调用Linux系统下令。bash提供了一个图形化界面,提拔交互速率。
Shell-bash的处置惩罚流程:






      • 从终端或控制台获取用户输入下令
      • 将用户输入下令进行解析,判定输入下令是否为内置下令
      • 若是内置下令,则直接实行;若不是内置下令,则bash在初始子历程上下文中加载和运行它
      • 判定程序的实行状态,若为前台历程则等待历程竣事;否则直接将历程放在背景实行,继续等待用户下一次输入。


6.3 Hello的fork历程创建过程

在shell中输入 ./hello *学号* *姓名* *手机号* 3  ,shell起首判定它不是内置下令,于是shell查找当前目录下的可实行文件hello,并将其调入内存,shell将它解释为系统功能函数并交给内核实行。Shell通过pid_t fork(void)函数创建一个子历程,子历程会获得与父历程捏造地址空间类似的一段数据结构的副本。父历程与子历程最大的不同在于他们分别拥有不同的PID。

图6-1

6.4 Hello的execve过程

调用fork函数创建新的子历程后,子历程会调用execve函数,在当前历程的上下文中加载并运行一个新程序hello。execve函数从不返回,它会清除该历程的代码和地址空间内的内容,然后初始化并运行该程序,通过跳转到程序的第一条指令或入口点。它会映射私有地域,如打开的文件、代码和数据段,然后映射公共地域。加载器随后跳转到程序的入口点,即设置PC指向_start地址。_start函数终极调用hello中的main函数,从而完成了子历程的加载过程。
6.5 Hello的历程实行

在现代系统上运行一个程序时,历程为用户提供一个假象:就似乎我们的程序似乎是系统中当前运行的唯一程序一样,我们的程序似乎是独占的使用处置惩罚器和内存,处置惩罚器似乎是无间断的实行我们程序中的指令,我们程序中的代码和数据似乎是系统内存中唯一的对象。这种处置惩罚结果的详细实现结果本身就是一个逻辑控制流,它指的是一系列可实行程序的计数器pc的值,这些计数值唯一的定义对应于那些包罗在程序的可实行文件目标对象中的可实行指令,大概说它指的是那些包罗在程序运行时可以动态通过链接触到可实行程序的共享文件对象的可实行指令。 历程的时间片是指一个历程在实行控制流时候所处的每一个时间段。

处置惩罚器通过设置在某个控制寄存器中的一个模式位来限定一个程序可以实行的指令以及它可以访问的地址空间。没有设置模式位时,历程就运行在用户模式中。用户模式下不答应实行特权指令,不答应使用大概访问内核区的代码大概数据。设置模式位时,历程处于内核模式,该历程可以访问系统中的任何内存位置,可以实行指令集中的任何下令。历程从用户模式变为内核模式的唯一方式是使用诸如终端、故障或陷入系统调用这样的非常。非常发生时,控制通报到非常处置惩罚程序,处置惩罚器从用户模式转到内核模式。 上下文在运行时候的状态这也就是一个历程内核重新开始启动一个被其他历程或对象库所抢占的网络服务器时,该历程所可能须要的一个下文状态。它由通用寄存器、浮点数据寄存器、程序实行计数器、用户栈、状态数据寄存器、内部多核栈和各种应用内核数据结构等各种应用对象的最大值数据寄存器组成。
在调用历程发送sleep之前,hello在当前的用户内核模式下历程继续运行,在内核中历程再次调用当前的sleep之后历程转入用户内核等待休眠模式,内核中全部正在处置惩罚等待休眠哀求的应用程序主动哀求释放当前正在发送处置惩罚sleep休眠哀求的历程,将当前调用hello的历程主动加入正在实行等待的队列,移除或退出正在内核中实行的历程等待队列。
设置定时器,休眠的时间即是自己设置的时间,当定时器时间到时,发送一个停止信号。内核收到停止信号进行终端处置惩罚,hello被重新加入运行队列,等待实行,这时候hello就可以运行在自己的逻辑控制流。

图6-2

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

hello的非常:在实行过程中,来自处置惩罚器外部I/O装备的信号的结果,例如Crtl-C、Crtl-Z等;或有意的实行指令的结果,如系统调用的等。

产生的信号:SIGINT,SIGSTP,SIGCONT,SIGWINCH


    • 正常运行:在等待程序进行十次循环竣事之后输入回车竣事程序:


图6-3



    • Ctrl+Z:在程序运行中途按下ctrl+z程序暂停并发出SIGSTP信号,不绝到接收到SIGCONT信号:


图6-4

由图6-4可以看出程序已经进行了5次循环,输入ps发现hello被挂起,再输入fg 2继续程序,程序将剩余五次循环跑完,按下回车键竣事程序:

图6-5

再输入kill -9 15554 杀死先前被挂起的另一个历程,输入ps进行查看:

图6-6



    • Ctrl+C:当按下crtl+c的时候,父历程会接收到SIGINT信号,使hello竣事运行,用调用wait等函数回收hello历程,使用ps查看,如图6-7所示,当前背景中已没有hello历程。


图6-7



    • 在运行过程中乱按键盘:对程序运行没有直接影响,getchar将读入一行输入,并且shell会将之后输入的字符串看成新的指令。


图6-8



    • Pstree:以树形结构显示程序和历程间的关系。


图6-9

6.7本章小结

本章详细先容了可实行文件hello是怎样被加载进内存实行的,其历程怎样在历程调度器调度下被CPU实行的,并详细先容了非常控制流,以及信号控制等。以hello程序为例,深入探索了hello历程的fork、execve过程及实行等过程。

7hello的存储管理


7.1 hello的存储器地址空间



    • 逻辑地址: 逻辑地址(Logical Address)是指由程序hello产生的与段相关的偏移地址部分(hello.o),它由选择符和偏移量组成。
    • 线性地址: 线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。详细的格式可以表示为:捏造地址形貌符:偏移量程序。它作为一个逻辑分页地址被输入到一个物理地址变换之间的一个中间层,在分页地址变换机制中须要使用一个线性分页地址形貌符作为输入,线性地址可以再颠末物理地址的变换以产生一个新的物理分页地址。再不同时启用一个分页地址机制的情况下,线性地址本身就是与捏造地址同意义的。hello的代码会产生逻辑地址,大概说是(即hello程序)段中的偏移地址,它加上相应段的基地址就生成了一个线性地址。
    • 捏造地址: 偶然我们也把逻辑地址称为捏造地址。因为与捏造内存空间的概念类似,逻辑地址也是与实际物理内存容量无关的,是hello中的捏造地址。
    • 物理地址: 物理地址(Physical Address)是指出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的终极结果地址。如果启用了分页机制,那么hello的线性地址会使用页目录和页表中的项变换成hello的物理地址;如果没有启用分页机制,那么hello的线性地址就是物理地址。

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

程序是由若干个逻辑分段组成的,如可由代码分段、数据分段、栈段、堆段组成。不同的段是有不同的属性的,所以就用分段(Segmentation)的形式把这些段分离出来。段式管理时实现Intel逻辑地址到线性地址转换机制的基础。
逻辑地址由【段选择符:偏移量】组成,线性地址为段首地址与逻辑地址中的偏移量组成。此中,段首地址存放在段形貌符中。而段形貌符存放在形貌符表中,也就是GDT(全局形貌符表)或LDT(局部形貌符表)中。在段选择符中,用TI全段选择附表的类型,在使用高13位的地址,找到段形貌符表,获得32位的段基址,再将段内偏移与段基址相加,获得32位线性地址,但是在现代Linux系统中,不适用分段机制,将前16位全设置为0,逻辑地址即线性地址。如下图7-1所示:

图7-1


图7-2

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

通太过页机制实现线性地址(捏造地址VA)到物理地址(PA)之间的转换,分页机制是指对捏造地址内存空间进行分页。
·起首Linux系统又自己的捏造内存系统,Linux将捏造内存构造成一些段的集合,段之外的捏造内存不存在因此不须要记录。
·内核为hello历程维护了一个段的使命结构即图中的task_struct,此中条目mm->mm_struct(形貌了捏造内存的当前状态),pgd->第一级页表的基地址(结合一个历程一串页表),mmap->vm_area_struct的链表。一个链表条目对应一个段,链表相连指出了hello历程捏造内存中的全部段。
image093.png
相关概念:
·捏造页(VP):系统将每个段分割为巨细固定的块,来作为进行数据传输的单元。对于Linux,每个捏造页巨细为4KB.
·物理页(PP/页帧):类似于捏造页,捏造内存也被分割。捏造内存系统中MMU负责地址翻译,MMU使用页表,即存一种放在物理内存中的数据结构,将捏造页到物理页映射,即捏造地址到物理地址的映射。
·通过页表机制寄存器PTBR+VPN在页表中获得PTE,PTE中包罗有效位、权限信息、物理页号。
如果有效位时0+NULL则代表没有在捏造内存空间中分配该内存;
如果时有效位0+非NULL,则代表在捏造内存空间中分配了但是没有被缓存到物理内存中;
如果有效位是1,则代表该内存已经缓存在了物理内存中,可以得到其物理页号PPN,与捏造页偏移量共同构成物理地址PA。
7.4 TLB与四级页表支持下的VA到PA的变换

按照上述模式,每次CPU产生一个捏造地址并且发送给地址管理单元,MMU就必须查找一个PTE行来用将捏造地址翻译成物理地址。为了消除这种操作带来的大量时间开销,MMU中设计了一个关于PTE的小的缓存,称为翻译后备缓冲器(TLB)也叫快表。例如当每次CPU发现须要重新翻译一个捏造地址是,它就必须发送一个VPN得到捏造地址MMU,发送一个VPO位得到一个L1高速缓存,例如当我们使用MMU向一个TLB的组哀求一个页表中的条目时,告诉缓存通过一个VPO位在页表中查找一个相应的数据标志组,并在页表中读出这个组里的个数据标志和相应的数据关键字,当MMU从一个TLB的组得到一个PPN时,代表缓存的工作在这个组的哀求之前就已经完全预备好,这个组的PPN与就已经可以与这些数据标志文件中的一个捏造地址进行很好的匹配。
Core i7采用四级页表条理结构,每个四级页表历程都有它自己的私有的页表条理结构,这种设计方法从两个基本方面就是减少了对内存的需求,如果一级页表的PTE全部为空,那么二级页表就不会继续存在,从而为历程节省了大量的内存,而且也只有一级页表才会有须要总是在一个内存中。四级页表的条理结构操作流程如下:36位虛拟地址被寄存器分别出来组成四个9位的片,每个片被寄存器用作到一个页表的偏移量。CR3寄存器内储存了一个L1页表的一个物理起始基地址,指向第一级页表的一个起始和终极位置,这个地址是页表上下文的一部分信息。VPN1提供了到一个L1PTE的偏移量,这个PTE寄存器包罗一个L2页表的起始基地址。VPN2提供了到一个L2PTE的偏移量,一共四级,逐级以此条理类推。

图7-3

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

以下格式自行编排,编辑时删除
物理地址的结构包括组索引位CI(倒数7-12位),使用它进行组索引,使用组索引位找到对应的组后,假设我们的cache采用8路的块,匹配标志位CT(前40位)如果匹配乐成且寻找到的块的有效位valid上的标志的值为1,则命中,根据数据偏移量CO(后6位)取出须要的数据然后进行返回。
如果没有数据被匹配乐成,大概匹配乐成但是标志位是1,这些都是未命中即miss的情况,此时向下一级缓存中查询数据(L2 Cache->L3 Cache->主存),并且将查找到的数据加载到Cache里,我们通常采用LRU策略,确定哪一个块作为牺牲快。
7.6 hello历程fork时的内存映射

捏造内存和内存映射解释了fork函数如作甚hello历程提供私有的捏造地址空间。fork为hello的历程创建捏造内存,创建当前历程的的mm_struct,vm_area_struct和页表的原样副本;两个历程中的每个页面都标志为只读;两个历程中的每个地域结构都标志为私有的写时复制,在hello历程中返回时,hello历程拥有与调用fork历程类似的捏造内存。随后的写操作通过写时复制机制创建新页面。
并且它还拥有自己独立的逻辑控制流,它同样可以拥有当前已经可以打开的各类文件信息和页表的原始数据和样本,为了有效掩护历程的私有数据和信息,同时为了节省对内存的消耗,历程的每个数据地域都被内核标志起来作为写时复制。
7.7 hello历程execve时的内存映射

在bash中的历程中实行execve的函数调用,execve函数在当前历程中加载并运行包罗在可实行文件hello中的程序,用hello更换了当前bash中的程序。
下面是加载并运行hello的几个步骤:
·删除已存在的用户地域:删除当前历程捏造地址的用户部分的已存在的地域结构。
·映射私有地域:为新程序的代码、数据、bss和栈地域创建新的地域结构,全部这些新的地域都是私有的、写时复制的。
·映射共享地域:hello程序与共享对象libc.so链接,libc.so是动态链接到这个程序的,然后再映射到用户捏造地址空间中的共享地域内。
·设置程序计数器(PC):execve会设置当前历程上下文的程序计数器,使之指向代码地域的入口点后,对该历程的调度就将重代码地域入口点开始,并根据须要通过缺页故障将磁盘中的数据与代码载入物理内存。

图7-4

7.8 缺页故障与缺页停止处置惩罚

处置惩罚缺页是由硬件和操作系统内核协作完成的。

图7-5

详细处置惩罚流程如下:






      • 处置惩罚器生成一个捏造地址,并将它传送给MMU。
      • MMU生成PTE地址,并从高速缓存/主存哀求得到它。
      • 高速缓存/主存向MMU返回PTE。
      • PTE中的有效位是0,所以MMU出发了一次非常,通报CPU中的控制到操作系统内核中的缺页非常处置惩罚程序。
      • 缺页处置惩罚程序确认出物理内存中的牺牲页,如果这个页已经被修改了,则把它换到磁盘。
      • 缺页处置惩罚程序页面调入新的页面,并更新内存中的PTE。
      • 缺页处置惩罚程序返回到原来的历程,再次实行导致缺页的下令。CPU将引起缺页的捏造地址重新发送给MMU。因为捏造页面已经换存在物理内存中,所以就会命中。


7.9动态存储分配管理

动态内存分配器维护着一个历程的捏造内存地域,称为堆。系统之间细节不同,但是不失通用性,假设堆是一个哀求二进制零的地域,它紧接在未初始化的数据地域后开始,并向上生长。对于每个历程,内核维护着一个变量 brk,它指向堆的顶部。分配器将堆视为一组不同巨细的块的集合来维护。每个块就是一个连续的捏造内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式实行的,要么是内存分配器自身隐式实行的。

分配器有两种基本风格:显式分配器,隐式分配器。两种分隔偶要求应用显示地分配块。它们的不同之处在于由哪个实体来负责释放已分配的块。
·显式分配器:要求应用显式地释放任何已分配的块。例如,c标准库提供一种叫做malloc程序包的显式分配器。c程序通过调用malloc函数来分配一个块,并通过调用free函数来释放一个块。c++中的new和delete操作符与c中的malloc和free相称。
·显示空闲链表:将空闲块构造为某种形式的显式数据结构。因为根据定义,程序不须要一个空闲块的主体,所以实现这个数据结构的指针可以存放在这些空闲块的主体内里。例如,堆可以构造成一个双向空闲链表,在每个空闲块中,都包罗一个pred(前驱)和succ(后继)指针。

图7-6  显式空闲链表

·隐式分配器:另一方面,要求分配器检测一个已分配块何时不再被程序所使用,那么就释放这个块。隐式分配器也叫做垃圾网络器,而主动释放未使用的已分配的块的过程叫做垃圾网络,例如,注入Lisp、ML以及Java之类的高级语言就依赖垃圾网络来释放已分配的块。
·分离适配:分配器维护着一个空闲链表的数组,每个空闲链表是和一个巨细类相关联的,并且被构造成某种类型的显式或隐式链表。每个链表包罗潜伏的巨细不同的块。malloc采用的就是分离适配的方法。

图7-7 隐式空闲链表

7.10本章小结

本章详细说明了hello的存储器地址空间;深入探讨了Intel逻辑地址到线性地址的变换;并对hello的线性地址到物理地址的变换进行了深入的解析;同时还细致分析了TLB与四级页表支持下的VA到PA的变换;三级Cache支持下的物理内存访问;hello在历程fork和execve的内存映射;以及缺页故障与缺页停止处置惩罚;最后详细分析了动态存储分配管理原理及方案。


8hello的IO管理


8.1 Linux的IO装备管理方法

以下格式自行编排,编辑时删除

  • 装备的模子化:文件
Linux装备管理的基本特点是把物理装备看成文件,采用处置惩罚文件的接口和系统调用来管理控制装备。一个Linux文件就是一个m字节的序列,全部I/O装备都被模子化为文件,乃至内核也如此。
文件的类型有:
普通文件:包罗任何数据,分成文本文件、二进制文件。
目录:包罗一组链接的文件。每个毗连都将一个文件名映射到一个文件
套接字:用于与另一个历程进行跨网络通讯的文件

  • 装备管理:unix io接口
将I/O装备模子化为文件的方式,答应Linux内核引出一个简单、低级的应用接口,称为Unix I/O,这使全部的输入、输出都能以一种同一且一致的方式来实行。我们可以对文件的操作有:打开关闭操作open和close;读写操作read和write;改变当前文件位置lseek等
8.2 简述Unix IO接口及其函数

Unix I\O一共实现了五个接口:打开文件,改变当前的文件位置,读文件,写文件和关闭文件。
Unix IO接口:


  • 打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个 I/O 装备,内核返回一个小的非负整数,叫做形貌符,它在后续对此文件的全部操作中标识这个文件,内核记录有关这个打开文件的全部信息,应用程序只须要记着这个形貌符。
  • linux shell 创建的每个历程开始时都有三个打开的文件:标准输入(形貌符为0) 、标准输出(形貌符为1) 和标准错误(形貌符为2) 。头文件< unistd.h> 定义了常量STDIN_FILENO 、STOOUT_FILENO 和STDERR_FILENO, 它们可用来取代显式的形貌符值。


  • 改变当前的文件位置:对于每个打开的文件,内核保持着一个文件位置 k,初始为 0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过实行 seek,显式地将改变当前文件位置 k。
  • 读写文件。一个读操作就是从文件复制n>0 个字节到内存,从当前文件位置k 开始,然后将k增长到k+n。给定一个巨细为m 字节的文件,当k~m时实行读操作会触发一个称为end-of-file(EOF)的条件,应用程序能检测到这个条件。在文件末端处并没有明确的“EOF 符号”。类似地,写操作就是从内存复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
  • 关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件。作为相应,内核释放文件打开时创建的数据结构,并将这个形貌符恢复到可用的形貌符池中。无论一个历程因为何种原因终止时,内核都会关闭全部打开的文件并释放它们的内存资源.
Unix IO函数:


  • 打开和关闭文件:历程是通过调用open函数来打开一个已经存在的文件大概创建一个新文件的,最后历程通过调用close函数关闭一个打开的文件。此中:filename被转换成一个文件形貌符并返回、flags参数指明历程打算怎样访问这个文件、mode参数指定了新文件的访问权限位:

图8-1



  • 读和写文件:应用程序通太过别调用read和write函数来实行输入和输出:

图8-2

*ssize_t和size_t的区别是size_t被定义为unsigned long而ssize_t为long。Read函数返回的是一个有符号的巨细,因为堕落时必须返回-1,而这一点使得read的最大值减小了一半。
8.3 printf的实现分析

以下格式自行编排,编辑时删除
·Printf在stdio.h中的代码段如下:

图8-3
va_list arg = (va_list)((char*)(&fmt) + 4);
va_list的定义:typedef char *va_list
这说明它是一个字符指针。此中的: (char*)(&fmt) + 4) 表示的是第一个参数。
C语言中,参数压栈的方向是从右往左。当调用printf函数的适合,先是最右边的参数入栈。fmt是一个指针,这个指针指向第一个const参数(const char *fmt)中的第一个元素。fmt也是个变量,它的位置,是在栈上分配的,它也有地址。 对于一个char *类型的变量,它入栈的是指针,而不是这个char *型变量。 ·Vsprintf的函数格式如下:

图8-4

该函数的作用是格式化,按照格式fmt结合参数生成格式化之后的字符串。返回生成字符串的长度。
write的汇编代码如下:

图8-5

一个int INT_VECTOR_SYS_CALL表示要通过系统来调用sys_call这个函数。所以它先将参数传入寄存器中之后调用。
syscall的实现如下:

图8-6

显示格式化了的字符串。
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析

getchar由宏实现:#define getchar() getc(stdin)它有一个int型的返回值。当程序调用getchar()时,它会等待用户按键来输入字符。用户输入的字符被存放在缓冲区中,直到用户按了回车键,这时getchar()才会从stdio流中读入一个字符。
getchar函数的返回值是用户输入的字符的ASCII码,若堕落返回-1,且将用户输入的字符回显到屏幕.若用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取.也就是说,后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为后,才等待用户按键。
这可以看做一个异步非常-键盘停止的处置惩罚:键盘停止处置惩罚子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。getchar函数调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结

程序的运行离不开输入和输出,本章详细形貌了Linux的IO装备的管理方法:装备的模子化和装备管理,进而分析了Unix I/O的接口机器函数,细致分析了printf和getchar函数的详细实现。至此,我们完成了hello的一生的分析。

结论

Hello是绝大部分程序员的第一个程序,它的代码非常简单,但实际在计算机中运行时它几乎要调动计算机上的软硬件协同合作。Hello的一生从编写到生成、运行和竣事的全过程可以分为几个阶段:
生成阶段
1. 编写源文件:起首,在文本编辑器中编写 `hello.c` 文件。
2. 预处置惩罚:`hello.c` 文件颠末预处置惩罚生成 `hello.i`。在这一步,预处置惩罚器会处置惩罚库的引用、宏变量的更换以及删除解释等工作。
3. 编译:`hello.i` 文件颠末编译生成 `hello.s`。这一阶段将C语言代码转换为汇编代码,同时分析全局变量的存储方式等细节。
4. 汇编:`hello.s` 文件颠末汇编生成可重定向文件 `hello.o`。在这一步,汇编代码被翻译成机器语言,即生成机器可以直接识别的程序。别的,为 `main` 函数的每一条指令分配了地址。
5. 链接:`hello.o` 文件通过链接各个静态库和动态库终极生成可实行文件 `hello`。在生成的 ELF 文件中,各个段都有了自己的捏造地址,并且 `hello.o` 文件中未确定位置的全局变量和函数都得到了重定位。
实行阶段
1. 启动实行:在 shell 中输入 `./hello` 并跟上三个参数 `200111028 伍文浩 3`。
2. 子历程与实行:shell 历程调用 `fork` 创建子历程,然后调用 `execve`。`execve` 调用启动加载器,映射捏造内存,创建新的内存地域,并创建一组新的代码、数据、堆和栈段。程序开始运行。
3. 实行指令:CPU 为该历程分配时间片,在一个时间片中,`hello` 程序可以控制 CPU 并次序实行自己的代码。
4. 动态内存申请:`printf` 会调用 `malloc` 向动态内存分配器申请堆中的内存。`hello` 访问内存时使用捏造内存机制,MMU 将程序中使用的捏造内存地址通过页表映射成物理地址。
5. 程序竣事:程序竣过后,shell 父历程会回收子历程。
6. 信号处置惩罚:在运行时,`hello` 还涉及信号机制。例如,`hello` 可以接受键盘输入 `Ctrl+Z` 引发停止非常挂起;调用 `exit` 函数引发陷阱;大概被 `Ctrl+C` 终止。
通过本次大作业,我从hello视角串联起计算机系统课程中的各章节知识,受益匪浅。
附件

hello.c : 源文件
hello.i : 颠末预处置惩罚生成的文件
hello.s : 颠末编译生成的文件
hello.o : 颠末汇编产生的可重定向文件
hello_o.elf : hello的ELF格式文件
hello_o.s: 使用objdump查看hello.o的反汇编代码
hello : 终极生成的可执举动文件
hello_elf:hello程序的elf格式文件
Hello_asm.s:hello反汇编生成的文件

参考文献


[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·奥哈拉伦著;深入理解计算机系统[M].北京:机器工业出书社,2016.7.
[8]  C library – C++ Reference .http://www.cplusplus.com/reference.
[9]  printf函数实现的深入剖析 .http://www.cnblogs.com/pianist/p/3315801.html.
[10] 机器码、指令与汇编语言的深度关系解析-百度开发者中央 .https://developer.baidu.com/article/detail.html?id=3131072.



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

使用道具 举报

0 个回复

倒序浏览

快速回复

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

本版积分规则

东湖之滨

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