目录
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P过程:
- 在编译中键入敲出代码得到hello.c程序
- 经过预处理器cpp生成hello.i
- hello.i再经过编译器ccl处理生成汇编程序hello.s
- hello.s经过汇编器as生成hello.o
- hello.o通过链接器ld与其他c语言库进行链接生成可执行目标文件hello
- 用户通过shell输入./hello后,就可以fork一个新的子进程,调用execve函数将程序加载到新的子进程中,以完成P2P,即Program to Process
020过程:
- 由shell通过fork和execve函数创建,shell为hello进程execve映射虚拟内存
- CPU为运行的hello分配时间片从而执行逻辑控制流
- 进程结束后,shell的父进程负责回收hello程序
- 内核删除相关数据结构
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
- 硬件环境:AMD Ryzen 5 5600H, 内存:16G, 硬盘:512G SSD
- 软件环境:Win11 x64,Ubuntu
- 开发环境:Visual Studio Code,gcc,gdb,edb
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
hello.i:预处理器处理hello.c后得到的文件
hello.s:编译器处理hello.i编译程序的汇编文件
hello.o:汇编器处理hello.s得到的可重定向目标文件
hello:链接器链接后得到的可执行文件
1.4 本章小结
本章介绍了P2P和020的概念,并对环境与工作及过程中的中间文件进行了分析。
第2章 预处理
2.1 预处理的概念与作用
概念:预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。
作用:使得程序便于阅读、修改、移植和调试,也有利于模块化程序设计
2.2在Ubuntu下预处理的命令
预处理命令:gcc -E -o hello.i hello.c
图1 预处理命令执行
2.3 Hello的预处理结果解析
hello.i中在原本hello.c的基础上对stdio.h、unistd.h、stdlib.h三个库进行了展开,将里面的函数复制了进去,并对原程序的缩进格式进行了一定的修改。
原来在.c文件中很少的几行在.i文件中变成3060行,因为预处理器在处理过程中实现了头文件的展开,宏的替换,并删除了注释信息。这就导致行数大大增加。而在最后才是我们原本的程序,与.c文件中的相差不大。
图2 部分库函数
图3 hello源代码
2.4 本章小结
本章主要是对预处理(包括头文件展开、宏替换、去掉注释、条件编译等等)的概念和作用进行介绍,除此之外还有Ubuntu下预处理的两个指令,同时也具体到了我的hello.c文件预处理结果hello.i文本文件的详细解析,深刻理解了预处理的重要内涵。
第3章 编译
3.1 编译的概念与作用
概念:编译程序是通过词法分析和语法分析,在确认所有的指令都符合语法规则后,将其翻译成等价的中间代码或汇编代码来表示。编译器(cc1)将预处理过的文本文件hello.i 翻译转换成汇编文本文件hello.s。
作用:将文本文件hello.i翻译成文本文件hello.s,并在出现语法错误时给出信息。
3.2 在Ubuntu下编译的命令
编译指令:gcc -S hello.i -o hello.s
图4 编辑结果
3.3 Hello的编译结果解析
3.3.1 数据:
程序没有全局变量,main函数的数据编译如下,要打印的字符串被编译成.string “Hello %s %s\n”,main函数被编译为全局,类型为function。
图5 数据
3.3.2 赋值:
通过mov指令完成,例赋值语句i = 0编译如下:
图6 赋值操作
3.3.3 算数操作:
通过汇编语言中一些算数操作指令完成,例如加法操作使用addl,例i++:
图7 算术操作
3.3.4 关系操作:
通过汇编语言cmp,test等指令完成,例:
图8 argc != 4
图9 i < 8
3.3.5控制转移:
if(argc!=4){…}编译如下:
图10 跳转控制
for 循环体控制:
图11 for循环体控制
3.3.6 数组操作:
源代码:
图12 数组
3.3.7 函数操作:
1、main函数:
图13 main函数操作
2、printf函数:
图14 printf函数操作示例
3、sleep函数操作
图15 sleep函数操作
3.4 本章小结
本章主要是对编译概念和作用进行详细介绍,同时也通过示例函数表明c语言是如何转换成为汇编代码的。通过学习汇编可以理解编译器的优化性能,并且分析解析出代码中所隐含的低效率从而更好地优化程序。
第4章 汇编
4.1 汇编的概念与作用
概念:指把汇编语言翻译成机器语言的过程。
作用:将hello.s汇编语言文件翻译成机器语言指令,并把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在二进制目标文件hello.o中。hello.o包含hello程序执行的机器指令。
4.2 在Ubuntu下汇编的命令
汇编指令:gcc -c hello.s -o hello.o
图16 汇编结果
4.3 可重定位目标elf格式
1、ELF头:
命令:readelf -h hello.o
开头是一个16字节的magic序列,描述了系统的字的大小和字节顺序。剩下包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。
图17 ELF头
2、节头:
命令:readelf -S hello.o
节头部分记录了各节的名称、类型、地址、偏移量、大小、全体大小、旗标、链接、信息、对齐等信息。使用节头表中的字节偏移信息可以得到各节在文件中的起始位置,以及各节所占空间的大小,这样方便重定位。
图18 节头
3、重定位节:
命令:readelf -r hello.o
重定位节中包含了.text 节中需要进行重定位的信息,我们可以发现需要重定位的函数有: .rodata, puts, exits, printf, atoi, sleep, getchar
图19 重定位节
4、符号表:
命令:readelf -s hello.o
符号表存放了程序中定义和引用的函数和全局变量的信息
图20 符号表
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
4.4 Hello.o的结果解析
命令:objdump -d -r hello.o
图21 反汇编结果
反汇编代码中,除了.s文件中已经出现过的代码,还包含了它们对应的机器语言的代码。比如说分支转移结构中,
hello.s表示为:
图22 .s-分支转移结构
而hello.o中表示为:
图23 .o-分支转移结构
这是因为.s文件中可以用段名称L3来进行助记,而在.o文件中需要它的真实地址以便于下一步操作。
而在函数调用方面,.s文件在call后可直接跟上函数名称,如call printf@PLT,但是.o文件call 后跟的是一条重定位条目指引的信息,如callq 62 <main+0x62>
4.5 本章小结
本章进行了hello.s的汇编,将其转换为二进制可重定位目标程序文件hello.o,通过readelf读取其elf信息与重定位信息,得到其符号表的相关信息,并通过objdump反汇编目标文件,从中得到机器代码,并将机器代码与汇编代码进行对照,发现机器语言与汇编语言存在一一映射关系。
第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
图24 链接指令
5.3 可执行目标文件hello的格式
生成elf格式命令:readelf -a hello > hello1.elf
- ELF头:
hello与hello.o的ELF头大致相同,不同之处在于hello的类型为EXEC可执行文件,有27个节
图25 ELF头
- 节头:
该节与hello.o的elf格式不同,在这里每一节都有了实际地址,说明重定向已经完成,没有.rel.各节,但保留了data text等节
每一条目中包含起始地址、偏移、大小等信息
图26 节头
- 重定位节:
图27 重定位节
- 符号表:
图28 符号表
- 程序头:
图29 程序头
5.4 hello的虚拟地址空间
虚拟地址段从0x401000到0x402000,与节头部表的.init相符。
图30 虚拟地址空间
5.5 链接的重定位过程分析
指令:objdump -d -r hello
图31 反汇编结果
不同:
1、hello.o的反汇编代码的地址从0开始,而hello的反汇编代码从0x400000开始。这说明hello.o还未实现重定位,采用的是相对偏移地址,而hello已经实现了重定位,采用的是虚拟地址。
图32 hello.o_差别1
图33 hello_差别1
2、hello的反汇编中多了很多别的函数的汇编代码,比如说.init节
图34 hello_差别2
重定位的过程:
- 重定位节和符号定义链接器将所有类型一致的节合并,赋予新的地址,那么程序中每条指令和全局变量都有唯一运行时的地址
- 如果遇见未知目标的引用,那么就新生成一个重定位条目。
5.6 hello的执行流程
(以下格式自行编排,编辑时删除)
通过edb的调试记录下每列函数调用call命令进入的函数如下所示:
图35 Ubuntu下edb运行程序
这其中的子函数名和地址(后6位)如下所示:
<_init> 401000 <exit@plt> 4010d0
<.plt> 401020 <puts@plt> 401090
<printf@plt> 4010a0 <getchar@plt> 4010b0
<atoi@plt> 4010c0 <sleep@plt> 4010e0
<_start> 4010f0 <main> 401125
<_dl_relocate_static_pie> 401120 <_fini> 401238
<__libc_csu_init> 4011c0 <__libc_csu_fini> 401230
5.7 Hello的动态链接分析
动态链接器使用过程链接表PLT和全局偏移量表GOT实现函数的动态链接。查阅节头可以得知,.got.plt起始位置为0x404000
在hello的elf中找到got表的位置为0x404000,可以看到在dl_init前后,其got[1]和got[2]的内容发生了变化。
图36 hello的elf中got表的位置
图37 调用前
图38 调用后
5.8 本章小结
本章分析了可执行文件hello的ELF格式及其虚拟地址空间,分析了重定位过程、加载以及运行时函数调用顺序以及动态链接过程,深入理解了链接和重定位的过程。
第6章 hello进程管理
6.1 进程的概念与作用
概念:进程是一个执行中的程序的实例,是系统进行资源分配和调度的基本单位。
作用:每一个程序都运行在某个进程的上下文中,上下文是用来保持程序的一些状态。进程给程序提供独立的逻辑控制流和私有的地址空间
6.2 简述壳Shell-bash的作用与处理流程
作用:shell-bash是一个用于与操作系统交互的程序,可用于创建进程,处理异常和信号等。
处理流程:
1.终端进程读取命令行。
2.分析命令行,获取命令行参数,并构造传递给execve的argv
3.检查第一个命令行参数是否是一个内置的shell命令,如果不是内部命令,调用fork( )创建新进程/子进程
4.在子进程中,用步骤2获取的参数,调用execve( )执行指定程序。
5.如果用户没要求后台运行否则shell使用waitpid等待作业终止后返回。
6.如果用户要求后台运行,则shell返回。
6.3 Hello的fork进程创建过程
fork创建过程:
Shell会调用fork函数创建一个子进程,内核会为新进程创建各种数据结构,并分配给它一个唯一的pid,在返回是,新进程的虚拟内存与调用fork的进程的虚拟内存相同。
6.4 Hello的execve过程
execve过程:
execve函数会根据文件名加载并运行程序,在虚拟内存中,它会删除用户区域,并映射私有区域,同时会设置程序计数器,将其指向该程序的入口点开始执行。
6.5 Hello的进程执行
1、hello的进程执行过程如下:
上下文信息:内核在一个进程正在运行的时候,调度了另一个新的进程运行后,它就抢占当前进程,并使用上下文切换来控制转移到新的进程。我们需要保存以前进程的上下文,恢复新恢复进程被保存的上下文,将控制传递给这个新恢复的进程来完成上下文切换。
2、进程时间片:
进程执行它的控制流的一部分的每一时间段。
3、进程调度的过程:
在进程执行时,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种行为就叫做调度。
4、用户态与核心态转换:
进程只有故障、中断或陷入系统调用时才会得到内核访问权限,否则将一直处于用户权限之中,以保证系统的安全性。
6.6 hello的异常与信号处理
1、hello执行过程中可能会出现的异常有:
中断、陷阱、故障和终止,可能产生的信号有SIGINT、SIGQUIT、SIGKILL、SIGTERM、SIGALRM、SIGCHLD、SIGSTOP等。处理方式可能是将程序挂起等待下一个信号来临,或终止程序。
2、正常情况:
图39 正常情况代码运行
3、乱按的情况:
图40 乱按
很明显乱按对于程序运行没有影响。
4、Ctrl-Z,Ctrl-C:
图41 Ctrl-Z,Ctrl-C
Ctrl-Z:使用SIGTSTP信号,停止前台作业
Ctrl-C:使用SIGINT信号终止前台进程
5、Ctrl-z后可以运行ps jobs pstree fg kill 等命令:
图42 jobs
图43 fg
图44 kill && ps
图45 pstree
6.7本章小结
本章了解了hello进程的执行过程,展示了hello进程的执行以及hello的异常和信号处理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
1、逻辑地址:又称段地址,是段寄存器的偏移地址,是程序徽标后出现在汇编代码中的地址。
2、线性地址:是一种中间地址,在地址空间中由连续的整数表示
3、虚拟地址:即为线性地址,为进程提供私有的地址空间,通过内存映射将虚拟内存空间与对象联系起来。
4、物理地址:CPU外部地址总线上的地址信号,是地址变换的最终结果地址
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址表示为[段标识符:段内偏移量],段标识符是一个16位长的字段(段选择符)。
1、根据段选择符的T1选择,当前要转换段为是GDT或LDT,再根据寄存器,得到其地址。
2、拿出段选择符中前13位,在这个数组中,查找到对应的段描述符,就可以得到Base。
3、将基地址与逻辑地址相加,获得线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
虚拟地址用VA来表示。处理虚拟地址即处理线性地址。VA分为虚拟页号(VPN)与虚拟页偏移量(VPO),CPU取出VPN,通过页表基址寄存器来定位页表条目,在有效位为1时,从页表条目中取出信息物理页号(PPN),通过将物理页号与虚拟页偏移量(VPO)结合,得到由物理地址(PPN)和物理页偏移量(PPO)组合的物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
TLB:处理器向MMU发送虚拟地址,MMU向TLB发送VPN请求储存其中的PTE缓存,随后返回PTE条目,若发生缺页,会启动缺页处理程序。
四级页表:会将VPN分为4段,分别为VPN1、VPN2、VPN3、VPN4,通过树结构对其进行查找,进而找到合适的PTE条目。
7.5 三级Cache支持下的物理内存访问
得到物理地址后,根据物理地址从cache中寻找。到了L1后,寻找物理地址检测是否命中,不命中则紧接着寻找下一级cache L2,接着L3,如果L3也不命中,则需要从内存中将对应的块取出放入cache中,直到出现命中。
7.6 hello进程fork时的内存映射
Shell会调用fork函数创建一个子进程,内核会为新进程创建各种数据结构,并分配给它一个唯一的pid,它将每个页面标记为只读,并将区域结构标记为写时复制,在fork函数返回时,新进程的虚拟内存与调用fork的进程的虚拟内存相同
7.7 hello进程execve时的内存映射
1. 删除已存在的用户区域。删除之前进程在用户部分中已存在的结构。
2. 映射私有区域。创建新的代码、数据、堆和栈段。虚拟地址空间的代码和数据区域被映射为hello文件的.txt和.data区。bss区域是请求二进制零的,映射匿名文件,其大小包含在hello文件中。栈和堆区域也是请求二进制零的,初始长度为零。
3. 映射共享区域。如果hello程序与共享对象链接,比如标准C库libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域。
7.8 缺页故障与缺页中断处理
缺页故障:
当指令引用一个相应的虚拟地址,而与改地址相应的物理页面不再内存中,会触发缺页故障。
处理方式:
缺页处理程序是系统内核中的代码,选择一个牺牲页,如果这个牺牲页被修改过,那么就将它交换出去,换入新的页并更新页表。当缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令再次发送VA到MMU,这次MMU就能正常翻译VA了。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆,分配器将堆视为一组不同大小的块来维护,每个块就是一个个连续的虚拟内存片,要么是已分配的,要么是空闲的。分配器有两种基本风格。两种风格都要求应用显示地分配块。
显式分配器:应用显示分配与释放,例如malloc、free函数
隐式分配器:也叫做垃圾收集器,检测不再使用时,就释放这个快。
Printf会调用malloc,请简述动态内存管理的基本方法与策略。
7.10本章小结
本章主要有关内存管理。介绍了hello程序的存储空间,如何得到最终的地址,介绍了hello的四级页表的虚拟地址空间到物理地址的转换,三级cashe的物理内存访问、进程 fork 时的内存映射、execve 时的内存映射、缺页故障与缺页中断处理、动态存储分配管理等内容。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
1、设备的模型化:文件
所有的I/O设备都被模型化为文件,甚至内核也被映射为文件。
2、设备管理:unix io接口
这种将设备优雅地映射为文件的方式,允许Linux系统内核引出一个简单、低级的应用接口,称为Unix I/O。
我们可以对文件的操作有:打开关闭操作open和close;读写操作read和write;改变当前文件位置lseek等等。
8.2 简述Unix IO接口及其函数
8.2.1 Unix IO 接口:
1、打开文件。一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个I/O设备,内核返回一个小的非负整数,叫做描述符,它在后续对此文件的所有操作中标识这个文件,内核记录有关这个打开文件的所有信息。
2、Linux Shell创建的每个进程都有三个打开的文件:标准输入,标准输出,标准错误。
3、改变当前的文件位置。对于每个打开的文件,内核保持着一个文件位置k,初始为0,这个文件位置是从文件开头起始的字节偏移量,应用程序能够通过执行seek,显式地将改变当前文件位置k。
4、读写文件。一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n,给定一个大小为m字节的而文件,当k>=m时,触发EOF。类似一个写操作就是从内存中复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
5、关闭文件。当应用完成了对文件的访问之后,它就通知内核关闭这个文件,作为响应,内核释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止时,内核都会关闭所有打开的文件并释放他们的内存资源。
8.2.2 Unix IO 函数:
1.open()函数:
功能描述:用于打开或创建文件,在打开或创建文件时可以指定文件的属性以及用户的权限等各种类型的参数;
函数原型:int open(const char *pathname,int flags,int perms);
参数:pathname:被打开的文件名(可包括路径名如"dev/ttyS0")flags:文件打开方式;
返回值:成功:返回文件描述符;失败:返回-1;
2.close()函数:
功能描述:用于关闭一个被打开的的文件;
所需头文件:#include <unistd.h>;
函数原型:int close(int fd);
参数:fd文件描述符;
函数返回值:成功:返回0;出错:返回-1;
3.read()函数:
功能描述:从文件读取数据;
所需头文件:#include <unistd.h>;
函数原型:ssize_t read(int fd, void *buf, size_t count);
参数:fd:将要读取数据的文件描述词;buf:指缓冲区,即读取的数据会被放到这个缓冲区中去;count:表示调用一次read操作,应该读多少数量的字符;
返回值:返回所读取的字节数;0(读到EOF);-1(出错);
4.write()函数:
功能描述:向文件写入数据;
所需头文件:#include <unistd.h>;
函数原型:ssize_t write(int fd, void *buf, size_t count);
返回值:写入文件的字节数(成功);-1(出错);
5.lseek()函数:
功能描述:用于在指定的文件描述符中将将文件指针定位到相应位置;
所需头文件:#include <unistd.h>,#include <sys/types.h>;
函数原型:off_t lseek(int fd, off_t offset,int whence);
参数:fd:文件描述符;offset:偏移量,每一个读写操作所需要移动的距离,单位是字节,可正可负(向前移,向后移);
返回值:成功:返回当前位移;失败:返回-1;
8.3 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;
}
printf程序按照格式fmt结合参数args生成格式化之后的字符串,并返回字串的长度。而vsprintf函数作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
write函数将buf中的i个元素写到终端。
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)
8.4 getchar的实现分析
当程序调用getchar时,程序等待用户按键,用户输入的字符被存放在键盘缓冲区中直到用户按回车(回车也在缓冲区中)。
当用户键入回车之后,getchar才开始从stdio流中每次读入一个字符。getchar函数的返回值是用户输入的第一个字符的ascii码,如出错返回-1,且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
本章主要介绍了Linux的IO设备管理方法、Unix IO接口及其函数,分析了printf和getchar函数的实现。
结论
Hello所经历的历程:
hello.c源文件通过预处理生成hello.i,这是处理后的文件,为了方便对其进行编译
hello.i文件通过编译生成hello.s,主要完成有高级语言到汇编语言的翻译。
hello.s汇编文件通过汇编变成hello.o可重定位的二进制文件,这一步为了方便下一步的编译
hello.o通过链接生成hello可执行文件,将可重定位文件链接,此程序可在shell中运行。
shell通过fork、execve函数创建新进程并运行hello程序,此时该程序完成虚拟内存映射,并将程序计数器指向hello程序的入口点
在程序运行过程中,会通过虚拟地址翻译为物理地址进行访问内存。
当程序结束后,shell通过waitpid进行子进程回收,hello不在占用处理器、储存资源
一个简单程序的执行,需要软件与硬件、软件与操作系统的交互与配合,看似简单,却异常复杂,在今后学习中,也应保持求实的精神,而不因想当然而导致无知,应怀有求真精神。
附件
列出所有的中间产物的文件名,并予以说明起作用。
hello.i:预处理器(cpp)处理后得到的文件,包含了头文件等
hello.s:编译器(cc1)处理后得到的文件,将高级语言翻译为编译语言
hello.o:汇编器(as)处理后得到的文件,为可重定位目标文件
hello:链接器(ld)连接后的到的可执行文件
hello.elf:由hello可执行文件生成的.elf文件
hello.asm:hello.o的反汇编文件
hello1.asm:反汇编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.