1 程序的编译过程
参考自: link.
首先提出几个问题:
程序为什么要被编译器编译之后才可以运行?
编译器在把C语言程序转换成可以执行的机器码的过程中做了什么?怎么做的?
最后编译出来的可执行文件里面是什么?除了机器码还有什么?他们怎么存放的?怎么组织的?
#include <stdio.h>是什么意思?把stdio.h包含进来意味着什么?C语言库又是什么?它怎么实现的?
不同的编译器(Microsoft VC、GCC)和不同的硬件平台(x86、SPARC、MIPS、ARM),以及不同的操作系统(Windows、Linux、UNIX、Solaris),最终编译出来的结果一样吗?为什么?
Hello World程序是怎么运行起来的?操作系统是怎么装载它的?他从哪里开始执行?到哪儿结束?main函数之前发生了什么?main函数结束之后又发生了什么?
如果没有操作系统,Hello World可以运行吗?如果要在一台没有操作系统的机器上运行Hello World需要什么?应该怎么实现?
printf是怎么实现的?他为什么可以有不定数量的参数?为什么它能够在终端上输出字符串?
Hello World程序在运行时,它在内存中是什么样子的?
(C其他拓展书籍)
编译一个C程序可以分为四阶段,预处理阶段->生成汇编代码阶段->汇编阶段->链接阶段,这里以linux环境下gcc编译器为例。使用gcc时默认会直接完成这四个步骤生成可以执行的程序,但通过编译选项可以控制进行某些阶段,查看中间的文件。
1 预处理
此阶段主要完成#符号后面的各项内容到源文件的替换,往往一些莫名其妙的错误都是出现在头文件中的,要在工程中注意积累一些错误知识。
(1)、#ifdef等内容,完成条件编译内容的替换
(2)、#include中内容,在当前目录或者指定目录,或者默认目录搜索头文件,并将头文件拷贝到源文件中。
(3)、#define的内容,替换define的内容(包括上一步的头文件中的define内容)
此阶段产生[.i]文件。
2 编译
此阶段完成语法和语义分析,然后生成中间代码,此中间代码是汇编代码,但是还不可执行,gcc编译的中间文件是[.s]文件。
在此阶段会出现各种语法和语义错误,特别要小心未定义的行为,这往往是致命的错误。
第一个阶段和第二个阶段由编译器完成。
3 汇编
此阶段主要完成将汇编代码翻译成机器码指令,并将这些指令打包形成可重定位的目标文件,[.O]文件,是二进制文件。
此阶段由汇编器完成。
4 链接
此阶段完成文件中叼用的各种函数跟静态库和动态库的连接,并将它们一起打包合并形成目标文件,即可执行文件。
此阶段由链接器完成。
gcc编译C语言主要用到以下几个程序:C编译器gcc、汇编器as、链接器ld和二进制转换工具objcopy。gcc命令只是一个后台程序的包装,会根据不同的参数要求去调用预编译编译程序cc1(c)、汇编器as、连接器ld
通过编译选项可以控制进行某些阶段,以产生中间文件:
gcc [选项] 要编译的文件 [选项] [目标文件]
其中,目标文件可缺省,gcc默认生成可执行的文件名为:a.out
gcc main.c 直接生成可执行文件a.out
gcc -E main.c -o hello.i 生成预处理后的代码(还是文本文件)
gcc –S main.c -o hello.s 生成汇编代码
gcc –c main.c -o hello.o 生成目标代码
2 C程序目标文件和可执行文件结构
参考自: link.
1、讲C语言内存管理的书籍或者博客?:https://www.zhihu.com/question/29922211
2、readelf命令: http://man.linuxde.net/readelf
3、面试官问我:bss段的大小记录在哪里?:http://bbs.csdn.net/topics/390613528
4、内存区划分、内存分配、常量存储区、堆、栈、自由存储区、全局区: http://www.cnblogs.com/CBDoctor/archive/2011/12/24/2300624.html
5、常量存在内存中的那里?:http://bbs.csdn.net/topics/390510503
linux下的可执行文件有三个段文本段(text)、数据段(data)、bss段,可用nm命令查看目标文件的符号清单。
编译过程:
(这个图是源程序与可执行文件的关系,即编译过程)
其中注意的BSS段,并没有保存未初始化段的映像,只是记录了该段的大小(应为该段没有初值,不管具体值),到了运行时再到内存为未初始化变量分配空间,这样可以节省目标文件(也可以理解为可执行文件)空间。对于data段,只是保存在目标文件中,运行时直接载入。
BSS是不占用可执行文件空间的,其内容由操作系统初始化(清零),DATA却需要占用,其内容由程序初始化。因此造成了上述情况。bss段(未手动初始化的数据)并不给该段的数据分配空间,只是记录数据所需空间的大小;bss段的大小从可执行文件中得到 ,然后链接器得到这个大小的内存块,紧跟在数据段后面。
运行过程:(可执行文件->内存空间)与上面的图区分
linux中的程序映像(载入内存后,也就是在内存中,也就是运行中):
下面解释这些段:
-
正文段(code segment/text segment,.text段):或称 代码段,通常是用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读,某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。CPU执行的机器指令部分。( 存放函数体的二进制代码 。)
-
只读数据段(RO data,.rodata):只读数据段是程序使用的一些不会被改变的数据,使用这些数据的方式类似查表式的操作,由于这些变量不需要修改,因此只需放在只读存储器中。
-
已初始化读写数据段(data segment,.data段):通常是用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。常量字符串就是放在这里的,程序结束后由系统释放(rodata—read only data)。已初始化读写数据段(RW data,.data):已初始化数据是在程序中声明,并且具有初值的变量,这些变量需要占用存储器空间,在程序执行时它们需要位于可读写的内存区域,并具有初值,以供程序读写。
只读数据段 和数据段统称为 数据段
-
BSS段(bss segment,.bss段):未初始化数据段(BSS,.bss)通常是指用来存放程序中未初始化的全局变量的一块内存区域。BSS是英文Block Started by Symbol的简称。BSS段属于静态内存分配。全局变量 和 静态变量 的存储是放在一块的。初始化的全局变量和静态变量在一块区域(.rwdata or .data),未初始化的全局变量和未初始化的静态变量在相邻的另一块区域(.bss), 程序结束后由系统释放。未初始化数据是在程序中声明,但是不具有初值的变量,这些变量在程序运行之前不需要占用存储空间。
* 在 C++中,已经不再严格区分bss和 data了,它们共享一块内存区域
* 静态存储区包括bbs段和data段 -
堆(heap):堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆上被剔除(堆被缩减)。一般由程序员分配释放(new/malloc/calloc delete/free),若程序员不释放,程序结束时可能由 OS 回收。注意:它与数据结构中的堆是两回事,但分配方式倒类似于链表
-
栈(stack):栈又称堆栈,是用户存放程序临时创建的局部变量,也就是我们函数大括号"{}"中定义的变量(不包括static声明的变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且等调用结束后,函数的返回值也会被存放在回栈中。由于栈的先进先出特性,所有栈特别方便用来保存/恢复调用现场。从这个意义上讲,把堆栈看成一个寄存、交换临时数据的内存区。由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈
例子:
int a = 0; // a 在 data
char *p1; // p1 在 bss
main()
{
int b; // b 在 stack
char s[] = "abc"; // s 在 stack, abc\0 在常量区
char *p2; // p2 在 stack
char *p3 = "123456"; // p3 在 stack, 123456\0 在常量区
static int c = 0; // c 在 data
p1 = (char *)malloc(10); // 申请的10字节内存在 heap, bss中的指针指向heap中的内存
p2 = (char *)malloc(20); // 申请的20字节内存在 heap, stack中的指针指向heap中的内存
strcpy(p1, "123456"); // 123456\0 在常量区,编译器可能会将它与 p3 所指向的 "123456\0" 优化成一块
}
其他链接、编译、汇编、预处理等更加详细的过程见:link.
3makefile的编写
详见陈皓makefile文档
3.1 makefile 的规则
这里要说明一点的是,clean 不是一个文件,它只不过是一个动作名字,有点像c 语言中的label 一样,其冒号后什么也没有,那么,make 就不会自动去找它的依赖性,也就不会自动执行其后所定义的命令。要执行其后的命令,就要在make 命令后明显得指出这个label 的名字。像clean 这种,没有被第一个目标文件直接或间接关联,那么它后面所定义的命令将不会被自动执行,不过,我们可以显示要make 执行。即命令——make clean ,以此来清除所有的目标文件,以便重编译。
3.2 使用变量
例子:
edit : main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
cc -o edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
main.o : main.c defs.h
cc -c main.c
kbd.o : kbd.c defs.h command.h
cc -c kbd.c
command.o : command.c defs.h command.h
cc -c command.c
display.o : display.c defs.h buffer.h
cc -c display.c
insert.o : insert.c defs.h buffer.h
cc -c insert.c
search.o : search.c defs.h buffer.h
cc -c search.c
files.o : files.c defs.h buffer.h command.h
cc -c files.c
utils.o : utils.c defs.h
cc -c utils.c
clean :
rm edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
我们在makefile 一开始就这样定义:
于是,我们就可以很方便地在我们的makefile 中以$(objects) 的方式来使用这个变量了。
3.3 让make 自动推导
只要make 看到一个.o 文件,它就会自动的把.c 文件加在依赖关系中,如果make 找到一个whatever.o ,那么whatever.c 就会是whatever.o 的依赖文件。并且cc -c whatever.c 也会被推导出来,于是,我们的makefile 再也不用写得这么复杂。
3.4 makefile内容
Makefile 里主要包含了五个东西:显式规则、隐晦规则、变量定义、文件指示和注释。