剖析Hello World

来源:互联网 发布:mac svn客户端使用教程 编辑:程序博客网 时间:2024/06/07 14:54

1.1:提出问题

 这里不是讲解C编程而是采取大家所熟知的 helloworld.c剖析构建和执行过程。

Eg: Hello示例:
#include <stdio.h>int main(int argc,char **argv){        printf("Hello World \n");        return 0;}

程序很简单,知道C语言的童鞋都会,但是只有一些问题需要你知道,你为什么要包含stdio.h的头文件——如果你只是在终端输入“man printf”输出结果并不会告诉我们为什么,因为输入“man printf”得到的输出结果是以PRINTF(1)标准结束的,这个命令是用户空间的命令/usr/bin/printf  它只对shell-scripts有用,因为有些指令或程序有一个以上的主题,他们位于不同的区段中,因此要查看较后的区段,可以在man中指定。

知道如何正确的使用用户手册(执行 man man 指令):
区段1  用户指令(执行程序或shell命令)
区段2  系统调用(内核提供功能)
区段3  程序库调用 (程序库的函数)
区段4  设备(通常在 /dev 目录下)
区段5  文件格式(eg:/etc/passwd)
区段6  游戏
区段7  其它
区段8  系统指令
区段9  内核内部指令

通过上面的列表我们可以知道,我们需要的内容在区段3中,如果它是一个单独的系统调用也可能在区段2中。
因此:

wangye@wangye:~$ man 3 printf

......SYNOPSIS       #include <stdio.h>       int printf(const char *format, ...);......

可以帮助你找到你需要的头文件。



1.2:剖析编译过程

编译Hello.c文件可以使用GUN Complier Collection(GCC)编译器。
使用“which gcc”可以帮你找到gcc的安装路径:
wangye@wangye:~$ which gcc/usr/bin/gcc
如果没有安装gcc 可在 root 下执行如下命令:
wangye@wangye:~$ aptitude search gcc......i   gcc                                        - GNU C 编译器                                        p   gcc-4.1                                    - GNU C 编译器                                        p   gcc-4.1-base                               - GNU 编译器套装 (基本包)                             p   gcc-4.1-doc                                - documentation for the GNU compilers (gcc, gobjc, g++p   gcc-4.1-locales                            - The GNU C compiler (native language support files)  .......
i表示已安装,p表示未安装。

安装命令如下:
root@wangye:/home/wangye# apt-get install gcc  gcc-4.1-doc 

这时我们可以编译我们的Hello.c了

wangye@wangye:~$ gcc -g Hello.c -o Hello  // -g表示使用gcc编译 HelloWorld.c 是源文件 “-o HelloWorld”                                                        表示生成目标文件,目标文件的名字为HelloWorldwangye@wangye:~$ ls
Hello Hello.c
执行:
wangye@wangye:~$ ./HelloHello World 
 


上面的内容看起来很简单,但是究竟发生了什么会出现这样的结果?

1.2.1:程序翻译:

计算机语言按照层次划分为:机器语言(0/1)、汇编语言(arm、x86、thumb)和高级语言(c++、java、c)。计算机真正执行的“程序流”是机器语言,早期的程序员都是用机器语言编程,后来为方便记忆发明了汇编语言,但仍然需记住很多硬件相关的操作指令,而且代码规模有限,人们又发明了高级语言。高级语言的出现才真正把程序员从千变万化的硬件操作中解脱出来。综上:编程语言越来越“智能”,抽象层次越来越多,生产率会越来越高。 我们的程序(高级语言)要想被计算机执行,就必须通过某种方式转化成机器语言(低级语言),然后打包成可执行目标程序(executable object program)(知道为什么windows的可执行文件都是.exe结尾了吧)。

以上述代码为例:"gcc  -o  hello  hello.c "   (-o hello表示生成的可执行文件名称为hello)代码转化过程如下:



1、预处理:把hello.c(机器无关)源程序中以“#”开头的预处理项,进行包含替换,生成预处理文件"test.i"(机器无关),当然具体工作还不只这些。例如:#include “stdio.h”就是把“stdio.h”文件内容替换到该行位置;总体说来就是不对代码进行任何转化,只进行替换和包含工作。验证结果如下:

输入“gcc -E hello.c -o hello.i”//gcc处理源程序,只进行预处理生成hello.i文件

输入“cat hello.i”   //显示hello.i文件内容

extern char *ctermid (char *__s) __attribute__ ((__nothrow__));# 886 "/usr/include/stdio.h" 3 4extern void flockfile (FILE *__stream) __attribute__ ((__nothrow__));extern int ftrylockfile (FILE *__stream) __attribute__ ((__nothrow__)) ;extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__));# 916 "/usr/include/stdio.h" 3 4# 2 "HelloWorld.c" 2int main(int argc,char **argv){        printf("Hello World \n");        return 0;}


  可以看到,以“#”开头的文件包含指令已经消失,被相应内容替换掉了,其他程序代码没有任何改变,注意:hello.i 仍然是文本文件,与hello.c 基本无异。


2、编译:根据处理器指令集(x86、mips、51、arm),把经预处理的“test.i”(机器无关)文件转换成汇编文件“test.s”(机器相关)。不同型号的处理器架构不同,造成他们之间的汇编指令集互不兼容,那同样一段高级程序生成的汇编程序就不相同;反过来讲,对于同一种处理器,即便选择的高级语言种类不同(c++、.net),编译也会产生相同的汇编程序。这么做的好处是:把高级语言和汇编语言之间的连线切断,就可以制造出不同类别的“编译程序”,只修改“编译程序”,就可以为不同的高级语言创建合适的编译系统。分层就有这个好处:抽象级别高,容易修改(只要接口一致即可)。验证结果如下:

输入命令“gcc -S hello.i -o hello.s” //把hello.i文件转换成hello.s文件

输入命令“cat hello.s”

.file"Hello.c".section.rodata.LC0:.string"Hello World ".text.globl main.typemain, @functionmain:pushl%ebpmovl%esp, %ebpandl$-16, %espsubl$16, %espmovl$.LC0, (%esp)callputsmovl$0, %eaxleaveret.sizemain, .-main.ident"GCC: (Debian 4.4.5-8) 4.4.5".section.note.GNU-stack,"",@progbits


可以看到,把与机器无关的代码“hello.i”进行转化,生成了机器相关的汇编文件“hello.s”。注意:“hello.s”仍然是文本文件,可以用vi直接编辑。

3、汇编:把与机器相关的汇编文件“hello.s”(文本文件)转化成机器相关的可执行(内含机器指令,也叫可重定位的目标文件)文件“hello.o”(二进制文件)。二进制文件hello.o基本上就十分接近可执行的机器文件了,但是还不能正常运行,需要经过下一步的“链接操作”才能真正被执行。验证结果如下:

输入“gcc -c hello.s -o hello.o” //把汇编文件转换成目标文件

输入“cat hello.o”

ELF�4(            U��������$��������Hello World GCC: (Debian 4.4.5-8) 4.4.5.symtab.strtab.shstrtab.rel.te80]AzzQ��ment.note.GNU-stack4<%PP0P$��HelloWorld.cmainputs

可以看到,这次已经无法用文本编辑器来正常解析出该文件了,因为二进制文件的解析方式不再是单个字节为单位,而是根据自己的规则调整的。

4、链接:把“hello.o”(二进制文件)转化成“hello”(可执行的二进制文件)。前面讲到“hello.o”几乎接近可执行二进制文件了,但仍然缺点什么。看第一幅图,在链接时,加入了printf.o文件,两个文件输出了一个文件。这是因为我们调用了printf库函数,该函数并不是我们自己定义的,而是在系统库中。既然想用这个函数,肯定也需要把这个函数的相关二进制代码也添加进来才行,这里就是做了这个工作。系统自己已经编译好了目标文件,我们只需要进行“链接”,把调用printf函数的地方,强制跳转到printf.o文件中即可,工作完成再跳回来,这就是为什么hello.o目标文件也叫可重定位文件,因为hello.o文件中还有一些执行代码的跳转地址并没有最终敲定。这里只有一个hello.c文件,如果自己定义了多个源文件,文件之间相互调用的话,也会进行这项工作的。

        把printf封装成库是有好处的,因为它是二进制文件(见上图)打开时乱码,这样的话别人就不能看到它的源代码,也就无法模仿可以保护知识产权;也能防止随意修改,造成系统错误。

输入“./ hello” //执行该程序,因为linux的权限管理较严格,即便是执行本目录下的程序也需要加上“./”表示本目录,windows不是这样,权限管理相对较松

系统输出:hello,word

        总结:这4个步骤下来,才算完成源程序向可执行程序的转化。中间各个步骤的工作在这里只是大致描述了一下,并不代表他们只做这些事情,具体工作还得研究相关编译器才行。