从一个简单的C语言代码,到可运行文件运行结束的过程中,充满了很多时候被隐藏起来封装好的步骤。我们很多时候都已经习惯了开箱即用,而忽视了我们所使用的程序内部是什么样的。此论文将会从各个方面讲解有关于Hello world程序的各个部分,那些在后台,甚至在暗处不为人所知的步骤。
关键词:计算机系统;
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第1章 概述
1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
-
-
- P2P
-
Hello程序的代码(Program)经过大致下面步骤编程一个可执行文件的。首先从hello.c文件开始,先通过预处理器得到修改了的源程序hello.i;然后,编译器(cc1)将其转化为汇编文件hello.s;接着,汇编器(as)将汇编文件转化为一个可重定位目标程序hello.o,接下来,链接器将一些文件与hello.o合并,得到hello。这些文件通常是标准C库中的函数对应的,预编译好了的目标文件。当我们运行时,shell会用fork()函数创建子进程,再用execve加载hello程序,这时,hello就由程序变成了一个进程(Process),完成了P2P的过程
-
-
- 020
-
开始时,hello并没有运行在系统中,相当于0。在shell中调用fork()函数创建子进程,再用execve加载可执行目标程序hello,映射虚拟内存。程序开始时载入物理内存,进入CPU处理。CPU为执行文件hello分配时间片,进行取值、译码、执行等流水线操作。内存管理器和CPU在执行过程中通过L1、L2、L3三级缓存和TLB多级页表在物理内存中存取数据,通过I/O系统进行输出。程序运行结束后,shell父进程会将该子进程进行回收,内核把它从系统中清除,这时hello就由0转化为了0,完成了020的过程
1.2 环境与工具
列出你为编写本论文,折腾Hello的整个过程中,使用的软硬件环境,以及开发与调试工具。
1.2.1. 硬件环境
处理器 Intel(R) Core(TM) i5-1035G1 CPU @ 1.00GHz 1.19 GHz
机带RAM 16.0 GB (15.8 GB 可用)
系统类型 64 位操作系统, 基于 x64 的处理器
1.2.2. 软件环境
VMWare 17.5.0 build-22583795
Ubuntu 22.04.3 LTS64位
1.2.3. 开发工具
Visual Studio 2022 64位;CodeBlocks 64位;vi/vim/gedit+gcc;
1.3 中间结果
列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。
1.4 本章小结
本章主要是概述内容,介绍了hello程序的P2P和020过程,也介绍了完成此次大作业所使用的硬件环境、软件环境以及开发工具。还列出了为完成本次大作业,生成的中间结果文件等。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
2.1.1. 预处理的概念
预处理是指预处理器根据预处理命令(以#开头),修改原始的C挨骂。比如hello.c中第一行中的#include <stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并直接插入到程序中。结果是得到了另一个C代码文件,以.i作为拓展名
2.1.2. 预处理的作用
1. 进行宏替换,例如替换掉#define所定义的宏。
2. 处理文件包含,将#include中包含的文件的内容插入到程序文本中。
3. 按照宏定义,使用#if 、#elif 、#else等,进行有选择的操作。
4. 删除C语言源程序中所有的注释
2.2在Ubuntu下预处理的命令
cpp hello.c -o hello.i
2.3 Hello的预处理结果解析
对比发现,hello.i中的代码相对于hello.c中只有几十行的代码量,达到了上千行。文件最后是去掉了注释的main()函数内容,上面要么是以#号开头的预处理内容,要么是引入的头文件内容
2.4 本章小结
本章介绍了概念、功能等有关于预处理的内容,尝试预处理得到源程序的预处理结果,并对其进行了分析。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
3.1.1. 编译的概念
编译是指将便于人编写、阅读、维护的高级计算机语言所写作的源代码程序,翻译为计算机能解读、运行的低阶机器语言的程序。在这里指编译器(cc1)将文本文件hello.i翻译成汇编语言程序hello.s的过程。
3.1.2. 编译的作用
编译的作用是将高级计算机语言所写作的源代码程序翻译为汇编语言程序,在这个过程中,会进行以词法分析、语法分析、语义分析来生成汇编语言程序,且编译器可能在这个过程中根据编译选项对程序进行一些适当的优化。
3.2 在Ubuntu下编译的命令
cc1 hello.i -o hello.s
3.3 Hello的编译结果解析
3.3.1. 数据
3.3.1.1 数字常量
在这里,将argc与4比较,数字常量4在hello.s中以立即数$4的方式出现。
在循环的判断条件内出现了数字常量7,。由于循环条件为小于8,编译器将其转化为小于等于7,在截图倒数第二行中。
3.3.1.2. 字符串常量
在hello.c文件中,有两个字符串,均出现在printf()函数内。
在汇编代码中,这两个字符串被存储在内存中的.rodata节常量区中
在调用printf()函数时,编译器将语句翻译为rip寄存器的偏移量形式表示,并将该偏移量存入rdi寄存器中,作为函数的第一个参数调用。
3.3.1.3. 局部变量
编译器通常将局部变量放在寄存器中或者栈中。
例如将寄存在寄存器edi中的int argc参数,放在-20(%rbp)位置(第21行)。将char** argv放在-32(%rbp)位置(第22行)
将局部变量int i放在栈中-4(%rbp)的位置(第30行)
3.3.2 赋值
在hello.c文件中,对i赋值0对应的汇编代码如下
3.3.3. 函数调用
这里只以一处函数调用为例。在hello.c文件中,调用atoi()函数的过程如下。
首先将%rdi的值设为argv[3],这样atoi()函数的第一个参数就是argv[3];然后调用atoi()函数
3.3.4. 算术操作
hello.c中,for循环中i++;语句对变量进行了算术操作。
这里执行了i的自增操作。
3.3.5. 关系操作
代码中涉及到了两处比较。但这里只用其中一处比较举例。
在这里,argc与4比较,对应的汇编代码已经在截图有标示。
3.3.6. 指针操作
在这里,对数组的访问变为3步。1. 取数组的首地址。2. 在首地址上加上偏移量,获得数组元素的地址。3. 访问该数组元素的地址。
3.3.7. 控制转移
这里的代码涉及到了多处跳转,分别如下
3.4 本章小结
本章主要介绍了编译的概念,以及编译的作用和功能。具体分析了不同的C语言代码会编译为什么样的汇编代码。
第4章 汇编
4.1 汇编的概念与作用
4.1.1. 汇编的概念
汇编是指将汇编语言程序经过编译器转化为二进制的机器语言指令,并将这些指令打包为可重定位目标程序的格式,并保存在目标文件.o中。
4.1.2. 汇编的作用
汇编的作用是把汇编语言翻译成机器语言,用二进制0和1代替汇编语言中的符号,即让它成为机器可以直接识别的程序。最后把这些指令打包成可重定位目标程序的格式,并保存在目标文件.o中
4.2 在Ubuntu下汇编的命令
4.3 可重定位目标elf格式
4.3.1. ELF头
hello.o的ELF格式的开头是一个以16字节的序列开始的ELF头。这个序列表述了生成该文件的系统信息,例如字的大小和字节顺序。ELF头剩下的部分帮助链接器进行语法分析和解释目标文件的信息,包括ELF头的大小、目标文件的类型、处理器体系结构、节头部表的文件偏移,以及节头部表中条目的大小和数量。
4.3.2. 节头部表
文件的节头部表描述了目标文件中的不同节的类型、地址、大小、偏移等信息,以及可以对各部分进行的操作权限。
4.3.3. 重定位节
重定位节中包含了.text 节中需要进行重定位的信息,我们可以发现需要重定位的函数有: .rodata, puts, exits, printf, atoi, sleep, getchar
4.3.4. 符号表
符号表存放了程序中定义和引用的函数和全局变量的信息。
4.4 Hello.o的结果解析
反汇编代码中,hello.s和hello.i中的代码也有少些不同。例如jmp指令,在.s文件中表示为jmp .L3,而在.o文件中,表示为jmp 80(用具体指令地址代替label)
再比如调用函数也有区别。
在.o文件中的机器码对应的操作数为0。这是因为重定向后再链接生成可执行文件后才会生成确定的地址。所以这里的相对地址都用0代替。
4.5 本章小结
本章进行了hello.s的汇编,将其转换为二进制可重定位目标程序文件hello.o,通过readelf读取其elf信息与重定位信息,得到其符号表的相关信息,并通过objdump反汇编目标文件,从中得到机器代码,并将机器代码与汇编代码进行对照,发现机器语言与汇编语言存在一一映射关系。
第5章 链接
5.1 链接的概念与作用
5.1.1. 链接的概念
将文件中调用的各种函数跟静态库及动态库链接,并将它们打包合并形成可执行文件的过程。
5.1.2. 链接的作用
链接完成符号解析和重定位,符号解析将代码中的每个符号引用和一个符号定义关联起来。重定位合并输入模块,并为每个符号分配运行时地址。
5.2 在Ubuntu下链接的命令
5.3 可执行目标文件hello的格式
hello与hello.o的elf头大致相同。不同支持在于hello的类型为EXEC可执行文件,并且有26个头。
其各段基本信息如下
5.4 hello的虚拟地址空间
虚拟地址段从0x401000到0x402000,与节头部表的.init相符。
5.5 链接的重定位过程分析
这里对比hello.o与hello的反汇编代码的区别。
hello.o反汇编代码的地址从0开始,而hello的反汇编代码从0x400000开始。这说明hello.o还未实现重定位,采用的是相对偏移地址,而hello已经实现了重定位,采用的是虚拟地址。
重定位后,重定位节和符号定义链接器将所有一直的节合并,并赋予新的地址。这样程序中每条指令和全局变量都有唯一一个运行时地址。如果遇见未知目标的引用,那么就新生成一个重定位条目。
5.6 hello的执行流程
通过edb的调试记录可知,每次调用call命令进入的函数如下所示。
<_init> <.plt> <printf@plt> <atoi@plt> <_start> <_dl_relocate_static_pie> <libc_csu_init> <exit@plt> <puts@plt> <getchar@plt> <sleep@plt> <main> <_fini> <__libc_csu_fini>
5.7 Hello的动态链接分析
查阅节头可知,.got.plt起始位置为0x404000。在调用dl_init前后,got[1]和got[2]的内容发生了变化。
5.8 本章小结
本章分析了可执行文件hello的ELF格式及其虚拟地址空间,分析了重定位过程、加载以及运行时函数调用顺序以及动态链接过程,深入理解了链接和重定位的过程。
第6章 hello进程管理
6.1 进程的概念与作用
6.1.1. 进程的概念
进程是一个执行中的程序的实例,是系统进行资源分配和调度的基本单位
6.1.2. 进程的作用
进程提供独立的逻辑控制流,好像程序在独占地使用处理器一样。也提供一个私有的地址空间,好像我们的程序独占地使用内存系统。
6.2 简述壳Shell-bash的作用与处理流程
6.2.1. 壳Shell-bash的作用
Shell为用户提供命令行界面,使用户可以在这个界面中输入shell命令,然后shell执行一系列的读/求值步骤,读步骤读取用户的输入的命令行,求值步骤则解析命令行,并运行程序。完成后重复上述步骤,直到用户退出shell。从而完成用户与计算机的交互来操作计算机。
6.2.2. 壳Shell-bash的处理流程
Shell等待用户输入指令。在用于输入指令后,从终端读取该命令并进行解析。若该命令为shell的内置命令,则立即执行该命令,否则,shell会通过fork创建一个子进程,通过execve加载并运行该可运行目标文件,用waitpid命令等待执行结束并对其进行回收,从内核中将其删除。若将该文件转为后台运行,则shell返回到循环的顶部,等待下一条命令。完成上述过程后,shell重复上述过程,直到用户退出shell。
6.3 Hello的fork进程创建过程
shell会调用fork函数创建一个子进程,内核会为新进程创建各种数据结构,并分配给它一个唯一的pid。新进程的虚拟内存与调用fork的进程的虚拟内存相同
6.4 Hello的execve过程
execve函数会根据文件名加载并运行程序。在虚拟内存中,它会删除用于区域,并映射私有区域,同时会设置程序计数器,
6.5 Hello的进程执行
当开始运行hello时,内存为hello分配时间片。如果一个系统运行多个进程,那么处理器的一个物理控制流被分成了多个逻辑控制流。逻辑流交错使用处理器。如果在此期间发生了异常或者系统中断,则内核会休眠该进程,并在核心态中进行上下文切换,将控制权交给其他进程。当hello执行到sleep时,hello会休眠,再次上下文切换,控制交付给其他进程。一段时间后再次上下文切换,恢复hello在休眠前的上下文信息,控制权交给hello继续执行。
hello在执行循环语句后,程序调用getchar(),hello从用户态进入核心态,并再次上下文切换,控制交付给其他进程。最终,内核从其他进程回到hello进程,在return后进程结束。
6.6 hello的异常与信号处理
6.6.1. 正常执行
正常执行时,hello每隔一秒打印一次,进入循环,共打印8次。打印完毕后,等待用户输入回车后程序终止。shell回收hello子进程,继续等待用户输入指令。
6.6.2. 不停乱按
乱按对于程序运行没有影响
6.3.3. 按ctrl+z
按下后,产生中断异常,此时hello的父进程shell接收到SIGSTP信号,运行信号处理程序。最终hello被挂起。
6.3.3.1. 输入ps
在此之后输入ps,打印出各个进程的pid,其中包括被挂起的hello。
6.3.3.2. 输入jobs
输入jobs,可以看到被挂起的hello的pid和它的状态。
6.3.3.3. 输入pstree -p
可以看到,从祖先进程到hello的树为:
systemd(1)->systemd(985)->gnome-terminal(2427)->bash(2459)->hello(26475)
6.3.3.4. 输入fg
输入fg后,挂在后台的hello进程被重新调到前台运行,并继续执行。
6.3.3.5. 输入kill
输入kill -9 26576,发送信号SIGKILL给进程26576,该进程被杀死。
6.3.4. 按ctrl-c
按下后,会导致段异常,从而内核信号产生SIGINT,发送给shell。shell收到该信号后,向子进程发送SIGKILL来强制终止子进程hello并回收它。这是再运行ps,发现已经没有进程helo,说明它已经被终止且回收。
6.7本章小结
本章总结了hello进程的执行过程,展示了hello进程的执行以及hello的异常和信号处理。
第7章 hello的存储管理
7.1 hello的存储器地址空间
7.1.1. 逻辑地址
又称段地址,是段寄存器的偏移地址,是程序汇编后出现在汇编代码中的地址。逻辑地址分为两部分,一个部分为段基址,另一个部分为段偏移量。在CPU保护模式下,需要经过寻址方式的计算和变换才可以得到内存中的有效地址。
7.1.2. 线性地址
是一种中间地址,在地址空间由连续的整数表示,是逻辑地址到物理地址变换之间的中间层。在各段中,逻辑地址是段中的偏移地址,其偏移量加上基地址就是线性地址。
7.1.3. 虚拟地址
是程序访问存储器使用的逻辑地址。使用虚拟地址时,CPU通过生成一个虚拟地址来访问主存,这个虚拟地址在被送至内存钱先转换成适当的物理地址。在linux中,虚拟地址数值等于线性地址
7.1.4. 物理地址
计算机系统的主存被组织称一个由一个由非常大个连续的字节大小的单元组成的数组,其中每一个字节都被给与一个唯一的物理地址。
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址表示为[段标识符:段内偏移量],段标识符是一个16位长的字段(段选择符)。
- 根据段选择符的T1选择,当前要转换段位是GDT还是LDT,再根据寄存器,得到其地址。
- 拿出段选择符的前13位,查找到对应的段描述符,得到Base。
- 将基地址和逻辑地址相加,获得线性地址。
7.3 Hello的线性地址到物理地址的变换-页式管理
虚拟地址用VA来表示。处理虚拟地址即处理线性地址。VA分为虚拟页号(VPN)与虚拟页偏移量(VPO)。CPU取出VPN,通过页表基址寄存器来定位页表条目,在有效位为1时,从页表条目中取出信息物理页号(PPN),通过将物理页号与虚拟页偏移量(VPO)结合,得到由物理地址和物理页偏移量(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后,寻找物理地址检测是否命中,如果不命中则寻找下一级L2,接着L3。如果L3也不命中,则需要从内存中取出相应的块并放入cache中,直到出现命中。
7.6 hello进程fork时的内存映射
fork()函数被父进程调用时,内核会创建一个子进程,为新的子进程创建环境,并分配给子进程唯一的pid,且该pid与父进程不同。为了给hello进程创建虚拟内存,fork()函数创建了当前进程的mm_struct、区域结构和页表的原样副本,并将两个进程的每个页都标记为只读,将两个进程中的区域结构都标记为私有的写时复制。当fork()从新的进程中返回时,hello进程现在的虚拟内存刚好和调用fork()时的虚拟内存相同,并且映射的也是同一个物理内存。但其中任意一个进程进行写操作时,写时复制机制就会创建新页面,在新的页面中进行写操作,并且原来的虚拟内存映射到创建的新页上,因此每个进程都具有私有的地址空间。
7.7 hello进程execve时的内存映射
加载并运行hello需要如下几个步骤。
7.7.1. 删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。
7.7.2. 映射私有区域。创建新的代码、数据、堆和栈段。虚拟地址空间的代码和数据区域被映射为hello文件的.text和.data区。
7.7.3. 映射共享区域。如果hello程序与共享对象(或目标)链接,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
7.7.4. 设置程序计数器。设置当前上下文的程序计数器,使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
当指令引用一个相应的虚拟地址,而与该地址相应的物理页面不在内存中,会触发缺页异常。缺页异常会调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,即存放在PP3的VP4。若VP4已经被修改了,那么内核就会将它复制回磁盘,否则直接修改。接下来,内存从磁盘复制VP3到内存中的PP3,更新PTE3,然后返回。当异常处理程序返回时,它会重新启动导致缺页的指令,该指令会将导致缺页的虚拟地址重新发送到地址翻译硬件。此时,VP3已经缓存在主存中,可以正常处理,而不会触发缺页。
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块来维护。每个块要么是已分配的,要么是空闲的。
7.10本章小结
本章主要介绍了hello的存储器地址空间,并介绍了逻辑地址、线性地址、虚拟地址、物理地址等概念。分析了段式管理是如何完成从逻辑地址到线性地址(虚拟地址)的变换的。分析了页式管理是如何完成线性地址到物理地址的变换。分析了TLB与四级页表支持下的VA到PA的变换。介绍了三级Cache支持下的物理内存访问的流程。分析了hello进程fork与execve时的内存映射。介绍了缺页故障和缺页中断的处理。分析了动态存储分配管理。
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
设备的模型化:文件
设备管理:unix io接口
在Linux中,所有的IO设备都被模型化为文件,而所有的输入和输出都被当做相应的文件的读写操作。将所有的输入和输出都能以一种统一且一致的方式来执行,这边是Linux的IO设备管理方法。
8.2 简述Unix IO接口及其函数
8.2.1. Unix IO接口
Linux将设备映射为文件,所有输入输出都当做文件读写来执行。
下面是Unix接口的操作。
- 打开文件
一个应用程序通过要求内核打开相应的文件,来宣告它想要访问一个IO设备。内存返回一个描述符,用于标识这个文件。内核记录和这个打开文件相关的所有信息。
- 每个进程开始时三个打开的文件
Linux shell 创建的每个进程都有3个打开的文件,分别为标准输入、标准输出和标准错误,即stdin、stdout、stderr。
- 改变当前的文件位置
对于每个打开的文件,内核保持着一个文件位置k,初始值为0。这个文件位置是从文件开始的字节偏移量。应用程序能够通过执行seek操作,显式地设置文件的当前位置k。
- 读写文件
读操作即从文件复制n个字节到内存。从当前文件位置k开始读入,然后将k增加到k+n。如果读到文件末尾,则触发EOF。同样,写操作时从内存中复制一些字节到一个文件,从当前文件k开始,然后更新k。
- 关闭文件
当应用完成了对文件的访问之后,它就会通知内核关闭这个文件。然后内核会释放文件打开时创建的数据结构,并将这个描述符恢复到可用的描述符池中。无论一个进程因为何种原因终止,内核都会关闭所有打开的文件并释放他们的内存资源。
8.3 printf的实现分析
https://www.cnblogs.com/pianist/p/3315801.html
首先观察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()的第一个参数为const char* 类型的fmt,而后面的参数用...代替。然后我们要设法得知传入参数的个数。
va_list arg = (va_list)((char*)(&fmt)+ 4);
这里可以看到arg是一个字符指针,其中(char*)((&fmt)+4)表示的是...中的第一个参数。这是因为C语言中,参数压栈的方向是从右向左的。也就是,先是最右面的参数入栈,然后是倒数第二个。接下来是
i=vsprintf(buf, fmt, arg);
首先查看一下vsprintf()的函数体。
int vs_printf(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;
break;
case ‘s’:
break;
default:
break;
}
}
return (p-buf);
经过分析发现,vsprintf()返回的是要打印的字符串的长度。后面一句
write(buf, i);
这句是写操作,将buf与参数数量i传入,将buf中的i个元素写到终端。然后我们观察一下write函数的实现。
write:
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
Int INT_VECTOR_SYS_CALL
接下来看sys_call的实现
sys_call:
call save
push dword [p_proc_ready]
sti
push ecx
push ebx
call [sys_call_table +eax * 4]
add esp, 4*3
mov [esi + EAXREG - P_STACKBASE], eax
cli
ret
在这里,sys_call实现了显示字符串。
8.4 getchar的实现分析
可以看到,getchar的实现是用一个宏实现的
#define getchar() getc(stdin)
getchar()有一个int类型的返回值。当程序调用getchar()时,程序等着用户按键。用户输入的字符被放在键盘缓冲区中,直到用户按下回车为止。当用户键入回车后,getchar()开始从stdin中每次读入一个字符。如果用户在按下回车之前输入了不止一个字符,那么其他字符会保留在键盘缓冲区中,等待后续的函数调用读取。
8.5本章小结
本章介绍了hello的IO管理方法,分析了printf()和getchar()函数的实现
结论
一个简单程序的执行,需要软件和硬件、软件与操作系统等的交互和配合。表面上简单,但实际上异常复杂。
附件
hello.i 预处理器处理后得到的文件。
hello.s 编译器处理后得到的文件,将高级语言翻译为汇编
hello.o 汇编器处理后得到的可重定位目标文件
hello 链接器链接后得到的可执行文件
hello_o_elf.txt 由hello.o得到的.elf文件
hello1_elf.txt 由hello得到的.elf文件
hello_o_s.txt 由hello.o得到的反汇编文件
hello_s.txt 由hello得到的反汇编文件
script.sh 用于生成hello所使用的脚本文件
参考文献
为完成本次大作业你翻阅的书籍与网站等