计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算学部
学 号 1190202407
班 级 1903005
学 生 李昊澎
指 导 教 师 史先俊
计算机科学与技术学院
摘 要
本文主要介绍在linux环境下hello.c的运行进程,按照预处理、编译、汇编、链接等各阶段,讲述其在linux下实现的原理。通过此程序,系统地回顾hello.c在linux下的生命周期,更好的理解计算机系统。
关键词:预处理、编译、汇编、链接、进程、存储、异常、I/O;
目 录
第1章 概述
1.1 Hello简介
P2P(From Program to Progress):编写程序得到.c文件,经过预处理、编译、汇编、链接,最终生成可执行目标文件。
O2O(From Zreo-0 to Zreo-0):shell执行可执行文件,管理hello进程,存储,并为其映射虚拟内存空间、分配物理内存,最后输出到屏幕。代码执行后,父进程回收hello进程。
1.2 环境与工具
(1)硬件环境:X64 CPU;2GHz;4GRAM;256Disk
(2)软件环境:Windows10 64位;Vmware 10;Ubuntu 16.04 LTS 64位
(3)使用工具:Codeblocks;Objdump;Gdb;Hexedit
1.3 中间结果
hello.c:源代码
hello.i:预处理后文本文件
hello.s:编译后汇编文件
hello.o:汇编后可重定位目标执行文件
hello:链接后可执行文件
hello.elf:hello.o的ELF格式
hello1.elf: hello的ELF格式
hello0.txt:hello.o反汇编代码
hello1.txt: hello反汇编代码
1.4 本章小结
本章主要介绍了hello的P2P,020过程,列举实验软硬件环境和工具以及生成的中间结果文件。
第2章 预处理
2.1 预处理的概念与作用
概念:程序设计领域中,预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。
作用:最常见的预处理是C语言和C++语言。ISO C和ISO C++都规定程序由源代码被翻译分为若干有序的阶段(phase) ,通常前几个阶段由预处理器实现。预处理中会展开以#起始的行,试图解释为预处理指令 (preprocessing directive) ,其中ISO C/C++要求支持的包括#if/#ifdef/#ifndef/#else/#elif/#endif(条件编译)、#define(宏定义)、#include(源文件包含)、#line(行控制)、#error(错误指令)、#pragma(和实现相关的杂注)以及单独的#(空指令)。预处理指令一般被用来使源代码在不同的执行环境中被方便的修改或者编译。
2.2在Ubuntu下预处理的命令
gcc -E -o hello.i hello.c
Hello.c:
Hello.i:
2.3 Hello的预处理结果解析
预处理得到的.i文件,发现头部扩展到了3000多行
2.4 本章小结
概括了预处理的概念和作用,详细说明了ubuntu下预处理命令,并分析了.i文件。
第3章 编译
3.1 编译的概念与作用
概念:广义的编译时将某一种程序语言写的程序翻译成等价的另一种语言。这里是将高级语言翻译成等价的汇编语言。
作用:编译是把高级语言转化为机器二进制代码的必经之路。它把高级语言翻译成更接近机器语言的汇编语言,使生成过程更加方便顺畅。
3.2 在Ubuntu下编译的命令
gcc -S -o hello.s hello.i
hello.s:
3.3 Hello的编译结果解析
1.数据类型
局部变量:int i
可看到,局部变量被存放到了rbp-4中。
局部变量:argc
可看到,argc存到寄存器edi中。
数组:argc[]
这个数组文件中存放的指针是输入字符串的指针,程序中两次对rax进行add操作,将相邻位置的数组元素argc[1],argc[2]的地址分别赋给rax,argv[3]的秒数作为字符串的秒数被第三次读取。
- 处理关系操作符与控制语句
程序中的if语言,argc!=4作为判断条件,编译器转换为汇编语言后变为:
argc先与4进行比较(cmpl),相等就跳转到.L2(je),否则向下执行。
- 运算
add:加 sub:减
此汇编语言中出现的add和sub语句主要是用来寻址。
- 函数处理
call语句用来进行函数调用,ret将返回值返回给函数。
(1)第一个printf转换成了puts,把.L0段的立即值传入%rdi,然后call 跳转到puts。
(2)exit把立即数1传入到%edi中,然后call跳转到exit
(3)第二个printf有三个参数,第一个是.LC1中的格式化字符串%eax中, 后面的两个依次是%rdi,%rsi,然后跳转到printf
(4)sleep有一个参数传到%edi中,之后call跳转到 sleep中
(5)getchar不需要参数,直接call跳转即可。
返回值:函数的返回值一般在寄存器%eax中,如果有返回值,则要先把 返回值存到%eax中,再用ret返回。源程序中有主函数的return 0;就是先把 返回值立即数0存到%eax中,再用ret返回。
3.4 本章小结
概括了编译的概念和作用,重点分析了c程序的数据与操作翻译成汇编语言时的表示和处理方法。
第4章 汇编
4.1 汇编的概念与作用
概念:汇编是指把汇编语言翻译成机器语言的过程,在这里还包括把这些机器语言指令打包成可重定位目标程序的过程。
作用:把汇编语言翻译成机器语言。
4.2 在Ubuntu下汇编的命令
as hello.s -o hello.o
4.3 可重定位目标elf格式
readelf -a hello.o > elf.txt
ELF头:概述ELF文件的各信息的段
Section Header:描述.o文件出现的各节的类型、位置和空间大小等信息。
.rel.text:.text节中位置的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改。另一方面,调用本地函数的指令则不需要修改。
.rela.eh_frame:.eh_frame节的重定位信息。
.symtab:一个符号表,它存放在程序定义和引用的函数和全局变量的信息。
4.4 Hello.o的结果解析
使用命令objdump -d -r hello.o > hellores.txt获得反汇编代码,与hello.s比较发现以下差别:
- 分支转移:在汇编代码中,分支跳转是直接以.L0等助记符表示,但是在反汇编代码中,分支转移表示为主函数+段内偏移量。
2)函数调用:汇编代码中函数调用时直接跟函数名称,而在反汇编的文件中call之后加main+偏移量(定位到call的下一条指令)。在.rela.text节中为其添加重定位条目等待链接。
3)访问全局变量:汇编代码中使用.LC0(%rip),反汇编代码中为0x0(%rip)。因为访问时需要重定位,所以初始化为0并添加重定位条目。
4.5 本章小结
概述了汇编的概念和作用,分析了ELF格式文件的内容,另外比较了重定位前汇编程序和重定位后反汇编的汇编程序差别,了解到从汇编语言翻译到机器语言的转换处理。
第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 >hello1.elf生成hello的ELF格式文件。
5.4 hello的虚拟地址空间
程序头:
PHDR 保存程序头表
INTERP 动态链接器的路径
LOAD 可加载的程序段
DYNAMIN 保存了由动态连接器使用的信息
NOTE 保存辅助信息
GNU STACK 标志栈是否可执行
GNU RELRO 指定重定位后需被设置成只读的内存区域
5.5 链接的重定位过程分析
objdump -d -r hello > helloout.txt
与hello.o生成的反汇编文件相比,helloout.txt中多了许多节。连接器通过把每个符合定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,从而重定位这些节。在使用ld命令链接的时候,指定了动态链接器为64的/lib64/ld-linux-x86-64.so.2。在动态共享库libc.so中,定义了hello需要的printf、sleep、getchar、exit 函数和_start 中调用的 __libc_csu_init,__libc_csu_fini,__libc_start_main。这些函数被链接器加入其中。
5.6 hello的执行流程
5.7 Hello的动态链接分析
调用共享库函数时,由于定义它的共享模块在运行时可以加载到任意位置,编译器没有办法预测这个函数的运行时地址。应为该引用生成一条重定位记录,然后动态链接器在程序加载时再解析它,找到它的地址。
5.8 本章小结
本章主要讲述从可重定位目标文件生成可执行文件的过程,回顾了链接的过程。
(
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
作用:进程作为一个执行中程序的实例,系统中每个程序都运行在某个进程的上下文中,上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
6.2 简述壳Shell-bash的作用与处理流程
Shell俗称壳,是"为使用者提供操作界面"的嵌入式软件。该软件提供了一种允许用户与其他操作系统之间进行通讯的一种方式。这种简单的通讯方式可以以交互方式,或者以交互方式shellscript(非交互)的方式允许用户执行。Shell—bash是一种交互型的应用级程序时Linux的外壳,它提供了一个界面,用户可以通过这界面访问操作系统内核。
流程:
1)从终端读入输入的命令
2)将输入字符串切分获得所有的参数
3)如果是内置命令则立即执行
4)否则调用相应的程序为其分配子进程并运行
5)shell应该接受键盘输入信号,并对这些信号进行相应处理
6.3 Hello的fork进程创建过程
Shell处理在终端输入的命令,判断不是内置命令后,会调用fork函数创建一个子进程,子进程几乎与父进程相同,但其pid与父进程不同。通过fork函数,子进程得到与父进程用户级虚拟地址空间相同的独立副本。
6.4 Hello的execve过程
调用fork函数后,子进程调用execve函数,该函数加载并运行可执行目标文件hello,并将参数argv和envp传递给main函数。
6.5 Hello的进程执行
进程上下文:进程的物理实体(代码和数据等)和支持进程的运行的环境合称为进程的上下文。而由进程的程序块、数据块、运行时的堆和用户栈(两者统称为用户堆栈)等组成的用户空间信息被称为用户级上下文。
由进程标识信息、进程现场信息、进程控制信息和系统内核栈等组成的内核空间信息被称为系统级上下文;处理器中各个寄存器的内容被称为寄存器上下文(也称硬件上下文),即进程的现场信息。
进程时间片:每个进程都在不同的时间段内运行,每个进程占用CPU的时间段。
用户态与核心态的转换:可以通过中断,陷阱,故障,终止等异常情况,程序会从用户态转换到内核态,内核保存进程的上下文,恢复下一个进程的上下文来重新启动该进程,控制转交给进程,从核心态转换到用户态。
hello的进程调度过程:限制性hello程序,进程在用户态,控制暂时在hello的进程中,当输入的参数数量不是3时,会调用exit函数结束进程,shell回收hello进程;当输入的参数数量为3时,会进入循环调用sleep函数,进程休眠一段时间,在这段时间内控制可能会交给内核,内核保存上下文,保包括I,sleepsecs的值,寄存器,栈,pc,条件码,state等值,然后恢复要进入进程的上下文,最后将控制传递给该进程,并开始计时,在休眠时间足够后,sleep会传送一个信号,可能会调用中断信号处理函数,将控制传递给内核,内核保存当前进程的上下文,恢复hello进程的上下文,将控制传递给hello进程,继续执行hello。
6.6 hello的异常与信号处理
运行过程中可能出现的异常种类由四种:中断、陷阱、故障、终止。
中断:来自I/O设备的信号,异步发生。硬件中断的异常处理程序被称为 中断处理程序。
陷阱:是执行一条指令的结果。调用后返回到下一条指令。
故障:由错误情况引起,可能能被修正。修正成功则返回到引起故障的指 令,否则终止程序。
终止:不可恢复的致命错误造成的结果,通常是一些硬件的错误,处理程 序会将控制返回给一个abort例程,该例程会终止这个应用程序。
6.7本章小结
本章介绍了进程的相关概念,介绍了fork函数和execve函数的作用于使用过程,介绍了shell的一般处理流程和异常信号处理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:逻辑地址用来指定一个操作数,它由选择符和偏移量组成。逻辑地址是程序代码在编译之后出现在汇编程序。
线性地址:逻辑地址向物理地址转化过程中的一步,逻辑地址经过段机制后转化为线性地址。
物理地址:计算机的主存被组织成一个由M个连续的字节大小单元组成的数组,每个字节都有一个唯一的物理地址。
虚拟地址:程序使用的地址,CPU通过生成一个虚拟地址来访问主存,这个虚拟地址被送到内存之前通过硬件与操作系统被转换成适当的物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
实地址模式:在实地址模式下,处理器使用20位的地址总线,可以访问1MB(0~FFFFF)内存。而8086的模式,只有16位的地址线,不能直接表示20位的地址,采用内存分段的解决方法。段地址存放于16位的段寄存器中(CS、DS、ES或SS)
保护模式:在保护模式下,段寄存器存放段描述符在段描述符表中的索引值,称为段选择器,此时CS存放代码段描述符的索引值,DS存放数据段描述符的索引值,SS存放堆栈段描述符的索引值
7.3 Hello的线性地址到物理地址的变换-页式管理
虚拟内存:虚拟内存是系统对主存的抽象概念,是硬件异常、主存、磁盘文件和内存文件的交互。它为每个进程提供了一个大的、一致的、私有的地址空间。
虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。每个字节都有一个唯一的虚拟地址,作为到数组的索引。磁盘上的数据被分割成块,这些块作为磁盘和主存(较高层)之间的传输单元。虚拟页则是虚拟内存被分割为固定大小的块。物理内存被分割为物理页,大小与虚拟页大小相同。
将各进程的虚拟空间划分成若干个长度相等的页,页式管理把内存空间按页的大小划分成片或者页面,然后把页式虚拟地址与内存地址建立一一对应页表,并用相应的硬件地址变换机构,来解决离散地址变换问题。页式管理采用请求调页或预调页技术实现了内外存存储器的统一管理。
7.4 TLB与四级页表支持下的VA到PA的变换
TLB(翻译后备缓冲器)是一个位于MMU中的小的虚拟地址的具有较高相联度的缓存,其每一行都是一组由数个PTE组成的块,TLB极大地减小了CPU访问PTE的开销,且能实现虚拟页面向物理页面的映射,同时对于页面数很少的页表可以完全包含在TLB中。
Cache命中情况
Cache不命中情况
7.5 三级Cache支持下的物理内存访问
Core i7内存系统:
Cache分为以下三类:
(1)直接映射高速缓存
直接映射高速缓存每个组只有一行,当CPU执行一条读内存字w的指令,它会向L1高速缓存请求这个字。如果L1高速缓存中有w的一个缓存副本,那么就会得到L1高速缓存命中,高速缓存会很快抽取出w,并将它返回给CPU。否则就是缓存不命中,当L1高速缓存向主存请求包含w的块的一个副本时,CPU必须等待。当被请求块最终从内存到达时,L1高速缓存将这个块存放在它的一个高速缓存行里,从被存储的块中抽取出字w,然后将它返回给CPU。确定是否命中然后抽取的过程分为三步:1)组选择;2)行匹配;3)字抽取。
组选择即从w的地址中间抽取出s个索引位,将其解释为一个对应组号的无符号整数,从而找到对应的组;行匹配即对组内的唯一一行进行判断,当有效位为1且标记位与从地址中抽取出的标记位相同则成功匹配,否则就得到不命中;而字选择即在行匹配的基础上通过地址的后几位得到块偏移,从而在高速缓存块中索引到数据。
(2)组相联高速缓存
组相联高速缓存每个组内可以多于一个缓存行,总体逻辑类似于直接映射高速缓存,不同之处在于行匹配时每组有更多的行可以尝试匹配,遍历每一行。如果不命中,有空行时也就是冷不命中则直接存储在空行;如果没有空行也就是冲突不命中,则替换已有行,通常有LFU(最不常使用)、LRU(最近最少使用)两者替换策略。
(3)全相联高速缓存
全相联高速缓存只有一个组,且这个组包含所有的高速缓存行(即E =C/B)。对于全相联高速缓存,因为只有一个组,组选择变的十分简单。地址中不存在索引位,地址只被划分为一个标记位和一个块偏移。行匹配和字选择同组相联高速缓存。
7.6 hello进程fork时的内存映射
Fork函数被shell进程调用是内核为hello新进程创建虚拟内存和各种数据结构并为其分配唯一的PID。为了给这个新进程创建虚拟内存,它创建了当前进程的mm_struct、区域结构和页表的原样副本。Shell将两个进程中每个页面都标为只读,并将每个进程中的每个区域结构标记为写时复制。
7.7 hello进程execve时的内存映射
execve函数在当前代码共享进程的上下文中加载并自动运行一个新的代码共享程序,它可能会自动覆盖当前进程的所有虚拟地址和空间,删除当前进程虚拟地址的所有用户虚拟和部分空间中的已存在的代码共享区域和结构,但是它并没有自动创建一个新的代码共享进程。新的运行程序仍然在堆栈中拥有相同的区域pid。之后为新运行程序的用户共享代码、数据、bss和所有堆栈的区域结构创建新的共享区域和结构,这一步叫通过链接映射到新的私有代码共享区域,所有这些新的代码共享区域都可能是在运行时私有的、写时复制的。它首先映射到一个共享的区域,hello这个程序与当前共享的对象libc.so链接,它可能是首先动态通过链接映射到这个代码共享程序上下文中的,然后再通过映射链接到用户虚拟地址和部分空间区域中的另一个共享代码区域内。为了设置一个新的程序计数器,execve函数要做的最后一件要做的事情就是自动设置当前代码共享进程上下文的一个程序计数器,使之成为指向所有代码共享区域的一个入口点(即_start函数)。
7.8 缺页故障与缺页中断处理
缺页异常及其处理过程:如果DRAM缓存不命中称为缺页。当地址翻译硬件从内存中读对应PTE时有效位为0则表明该页未被缓存,触发缺页异常。缺页异常调用内核中的缺页异常处理程序。
缺页处理程序搜索区域结构的链表,把A和每个区域结构中的vm_start和vm_end做比较,如果指令不合法,缺页处理程序就触发一个段错误、终止进程。
缺页处理程序检查试图访问的内存是否合法,如果不合法则触发保护异常终止此进程。
缺页处理程序确认引起异常的是合法的虚拟地址和操作,则选择一个牺牲页,如果牺牲页中内容被修改,内核会将其先复制回磁盘。无论是否被修改,牺牲页的页表条目均会被内核修改。接下来内核从磁盘复制需要的虚拟页到DRAM中,更新对应的页表条目,重新执行导致缺页的指令。
以下为Linux缺页处理简图:
7.9动态存储分配管理
在程序运行时程序员使用动态内存分配器 (比如malloc) 获得虚拟内存,动态内存分配器维护着进程的一个虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护,每个块要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
分配器分为显式分配器和隐式分配器。
显式分配器:要求应用显式地释放任何已分配的块。
隐式分配器:要求分配器检测一个已分配块何时不再使用,那么就释放这个块,自动释放未使用的已经分配的块的过程叫做垃圾收集。而自动释放未使用的已分配的块的过程叫做垃圾收集。
7.10本章小结
本章了解了存储器地址空间,段式管理和页式管理,介绍了动态内存分配和进程的创建及fork和execve时的内存映射,还介绍了缺页故障和缺页中断处理。分析了虚拟地址,线性地址和虚拟物理线性地址之间的互相转换,页表的命中与不页表的命中,使用动态快表缓存作为页表的高速缓存以及如何加速页表,动态内存管理的操作,fork时的动态内存中断与映射、execve时的动态内存中断与映射、缺页的中断与缺页映射和中断的处理。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:一个Linux文件就是一个M个字节的序列,所有的IO设备都被模型化为文件,而所有的输入和输出都被当做对相应文件的读和写来执行,这种将设备优雅地映射为文件的方式,允许 Linux 内核引出一个简单低级的应用接口,称为 Unix I/O。
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
打开文件:int open(char *filename,int flags,mode_t mode);
open函数将filename转换为一个文件描述符,并且返回描述符数字,返回的描述符总是在进程中当前没有打开的最小描述符,flags参数指明了进程打算如何访问这个文件,而mode参数指定了新文件的访问权限位。
关闭文件:int close(int fd);
fd是需要关闭的文件的描述符,close返回操作结果。
读文件:ssize_t read(int fd,void *buf,size_t n)
read函数从描述符为fd的当前文件位置赋值最多n个字节到内存位置buf。返回值-1表示一个错误,0表示EOF,否则返回值表示的是实际传送的字节数量。
写文件:ssize_t write(int fd,const void *buf,size_t n);
write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。write函数从内存位置buf复制至多n个字节到描述符为fd的当前文件位置。
8.3 printf的实现分析
先查看printf函数的函数体:
va_list定义typedef char * va_list,因此通过调用va_start函数,获得的arg为第一个参数的地址。
vsprintf返回的是要打印出来的字符串的长度,它的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
write函数:
根据此段代码,内核向寄存器传递参数后,调用syscall函数,所以接下来我们查看syscall函数。
syscall 将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII 码。
8.4 getchar的实现分析
异步异常-键盘中断的处理:在用户敲击或者按键盘上面的按钮的时候,键盘接口获得一个键盘扫描码,这样会在此时同时产生一个中断的请求,这会调用键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区的内部。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章主要介绍了Linux的IO设备管理方法、Unix IO接口及函数,分析了printf和getchar函数。
结论
Hello的历程:
1.编写代码:用高级语言写.c文件
2.预处理:从.c生成.i文件,将.c中调用的外部库展开合并到.i中
3.编译:由.i生成.s汇编文件
4.汇编:将.s文件翻译为机器语言指令,并打包成可重定位目标程序hello.o
5.链接:将.o可重定位目标文件和动态链接库链接成可执行目标程序hello
6.运行:在shell中输入命令
7.创建子进程:shell用fork为程序创建子进程
8.加载:shell调用execve函数,将hello程序加载到该子进程,映射虚拟内存
9.执行指令:CPU为进程分配时间片,加载器将计数器预置在程序入口点,则hello可以顺序执行自己的逻辑控制流
10.访问内存:MMU将虚拟内存地址映射成物理内存地址,CPU通过其来访问
11.动态内存分配:根据需要申请动态内存
12.信号:shell的信号处理函数可以接受程序的异常和用户的请求
13.终止:执行完成后父进程回收子进程,内核删除为该进程创建的数据结构
至此,hello运行结束
附件
hello.i 预处理得到的文本文件
hello.s 编译得到的汇编程序
hello.o 汇编后生成的可重定位目标文件
hello 连接后生成的可执行目标文件
Hello1.elf hello的elf文件
helloout.txt hello的反汇编文件
elf.txt hello.o的elf格式,分析汇编器和链接器行为
objdump.txt hello.o的反汇编,主要是为了分析hello.o
参考文献
为完成本次大作业你翻阅的书籍与网站等
[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.
(参考文献0分,缺失 -1分)