C/C++程序编译步骤详解

转载 2007年10月10日 09:55:00
C/C++语言很多人都比较熟悉,这基本上是每位大学生必学的一门编程语言,通常还都是作为程序设计入门语言学的,并且课程大多安排在大一。刚上大学,孩子们还都很乖,学习也比较认真,用心。所以,C/C++语言掌握地也都不错,不用说编译程序,就是写个上几百行的程序都不在话下,但是他们真的知道C/C++程序编译的步骤么?

我想很多人都不甚清楚,如果他接下来学过“编译原理”,也许能说个大概。VC的“舒适”开发环境屏蔽了很多编译的细节,这无疑降低了初学者的入门门槛,但是也“剥夺”了他们“知其所以然”的权利,致使很多东西只能死记硬背,遇到相关问题就“丈二”。实际上,我也是在学习Linux环境下编程的过程中才逐渐弄清楚C/C++源代码是如何一步步变成可执行文件的。

总体来说,C/C++源代码要经过:预处理编译汇编连接四步才能变成相应平台下的可执行文件。大多数时候,程序员通过一个命令就能完成上述四个步骤。比如下面这段C的“Hello world!”代码:

File: hw.c

#include <stdio.h>

int main(int argc, char *argv[])
{
        printf("Hello World!/n");

        return 0;
}


如果用gcc编译,只需要一个命令就可以生成可执行文件hw:

xiaosuo@gentux hw $ gcc -o hw hw.c

xiaosuo@gentux hw $ ./hw Hello World!


我们可以用-v参数来看看gcc到底在背后都做了些什么动作:

Reading specs from /usr/lib/gcc/i686-pc-linux-gnu/3.4.6/specs
Configured with: /var/tmp/portage/sys-devel/gcc-3.4.6-r2/work/gcc-3.4.6/configure --prefix=/usr --bindir=/usr/i686-pc-linux-gnu/gcc-bin/3.4.6 --includedir=/usr/lib/gcc/i686-pc-linux-gnu/3.4.6/include --datadir=/usr/share/gcc-data/i686-pc-linux-gnu/3.4.6 --mandir=/usr/share/gcc-data/i686-pc-linux-gnu/3.4.6/man --infodir=/usr/share/gcc-data/i686-pc-linux-gnu/3.4.6/info --with-gxx-include-dir=/usr/lib/gcc/i686-pc-linux-gnu/3.4.6/include/g++-v3 --host=i686-pc-linux-gnu --build=i686-pc-linux-gnu --disable-altivec --enable-nls --without-included-gettext --with-system-zlib --disable-checking --disable-werror --enable-secureplt --disable-libunwind-exceptions --disable-multilib --disable-libgcj --enable-languages=c,c++,f77 --enable-shared --enable-threads=posix --enable-__cxa_atexit --enable-clocale=gnu
Thread model: posix
gcc version 3.4.6 (Gentoo 3.4.6-r2, ssp-3.4.6-1.0, pie-8.7.10)
 /usr/libexec/gcc/i686-pc-linux-gnu/3.4.6/cc1 -quiet -v hw.c -quiet -dumpbase hw.c -mtune=pentiumpro -auxbase hw -version -o /tmp/ccYB6UwR.s
ignoring nonexistent directory "/usr/local/include"
ignoring nonexistent directory "/usr/lib/gcc/i686-pc-linux-gnu/3.4.6/../../../../i686-pc-linux-gnu/include"
#include "..." search starts here:
#include <...> search starts here:
 /usr/lib/gcc/i686-pc-linux-gnu/3.4.6/include
 /usr/include
End of search list.
GNU C version 3.4.6 (Gentoo 3.4.6-r2, ssp-3.4.6-1.0, pie-8.7.10) (i686-pc-linux-gnu)
        compiled by GNU C version 3.4.6 (Gentoo 3.4.6-r2, ssp-3.4.6-1.0, pie-8.7.9).
GGC heuristics: --param ggc-min-expand=81 --param ggc-min-heapsize=97004
 /usr/lib/gcc/i686-pc-linux-gnu/3.4.6/../../../../i686-pc-linux-gnu/bin/as -V -Qy -o /tmp/ccq8uGED.o /tmp/ccYB6UwR.s
GNU assembler version 2.17 (i686-pc-linux-gnu) using BFD version 2.17
 /usr/libexec/gcc/i686-pc-linux-gnu/3.4.6/collect2 --eh-frame-hdr -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o hw /usr/lib/gcc/i686-pc-linux-gnu/3.4.6/../../../crt1.o /usr/lib/gcc/i686-pc-linux-gnu/3.4.6/../../../crti.o /usr/lib/gcc/i686-pc-linux-gnu/3.4.6/crtbegin.o -L/usr/lib/gcc/i686-pc-linux-gnu/3.4.6 -L/usr/lib/gcc/i686-pc-linux-gnu/3.4.6 -L/usr/lib/gcc/i686-pc-linux-gnu/3.4.6/../../../../i686-pc-linux-gnu/lib -L/usr/lib/gcc/i686-pc-linux-gnu/3.4.6/../../.. /tmp/ccq8uGED.o -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/lib/gcc/i686-pc-linux-gnu/3.4.6/crtend.o /usr/lib/gcc/i686-pc-linux-gnu/3.4.6/../../../crtn.o


稍微整理一下,去掉一些冗余信息后,如下:

cc1 hw.c -o /tmp/ccYB6UwR.s
as -o /tmp/ccq8uGED.o /tmp/ccYB6UwR.s
ld -o hw /tmp/ccq8uGED.o


以上三个命令分别对应于编译步骤中的预处理+编译、汇编和连接。预处理和编译还是放在了一个命令(cc1)中进行的,可以把它再次拆分为以下两步:

cpp -o hw.i hw.c
cc1 hw.i -o /tmp/ccYB6UwR.s


一个精简过的能编译以上hw.c文件的Makefile如下:

.PHONY: clean

all: hw

hw: hw.o
        ld -dynamic-linker /lib/ld-linux.so.2 -o hw /usr/lib/crt1.o /
                /usr/lib/crti.o /
                /usr/lib/gcc/i686-pc-linux-gnu/3.4.6/crtbegin.o /
                hw.o -lc /
                /usr/lib/gcc/i686-pc-linux-gnu/3.4.6/crtend.o /
                /usr/lib/crtn.o

hw.o: hw.s
        as -o hw.o hw.s

hw.s: hw.i
        /usr/libexec/gcc/i686-pc-linux-gnu/3.4.6/cc1 -o hw.s hw.c

hw.i: hw.c
        cpp -o hw.i hw.c

clean:
        rm -rf hw.i hw.s hw.o


当然,上面Makefile中的一些路径是我系统上的具体情况,你的可能与我的不同。

接下来我们按照编译顺序看看编译器每一步都做了什么。

首先是预处理,预处理后的文件hw.i:

# 1 "hw.c"
# 1 "<built-in>"
# 1 "<command line>"

...
__extension__ typedef __quad_t __off64_t;
__extension__ typedef int __pid_t;
__extension__ typedef struct { int __val[2]; } __fsid_t;

...
extern int remove (__const char *__filename) __attribute__ ((__nothrow__));

extern int rename (__const char *__old, __const char *__new) __attribute__ ((__nothrow__));

...

int main(int argc, char *argv[])
{
 printf("Hello World!/n");

 return 0;
}


:由于文件比较大,所以只留下了少部分具有代表性的内容。

可以看见预处理器把所有要包含(include)的文件(包括递归包含的文件)的内容都添加到了原始的C源文件中,然后把其输出到输出文件,除此之外,它还展开了所有的宏定义,所以在预处理器的输出文件中你将找不到任何宏。这也提供了一个查看宏展开结果的简便方法。

第二步“编译”,就是把C/C++代码“翻译”成汇编代码:

.file "hw.c"
        .section .rodata
.LC0:
        .string "Hello World!/n"
        .text
.globl main
        .type main, @function
main:
        pushl %ebp
        movl %esp, %ebp
        subl $8, %esp
        andl $-16, %esp
        movl $0, %eax
        addl $15, %eax
        addl $15, %eax
        shrl $4, %eax
        sall $4, %eax
        subl %eax, %esp
        subl $12, %esp
        pushl $.LC0
        call printf
        addl $16, %esp
        movl $0, %eax
        leave
        ret
        .size main, .-main
        .section .note.GNU-stack,"",@progbits
        .ident "GCC: (GNU) 3.4.6 (Gentoo 3.4.6-r2, ssp-3.4.6-1.0, pie-8.7.10)"


这个汇编文件比预处理后的C/C++文件小了很多,去除了很多不必要的东西,比如说没用到的类型声明和函数声明等。

第三步“汇编”,将第二步输出的汇编代码翻译成符合一定格式的机器代码,在Linux上一般表现为ELF目标文件。

xiaosuo@gentux hw $ file hw.o
hw.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped


最后一步“连接”,将上步生成的目标文件和系统库的目标文件和库文件连接起来,最终生成了可以在特定平台运行的可执行文件。为什么还要连接系统库中的某些目标文件(crt1.o, crti.o等)呢?这些目标文件都是用来初始化或者回收C运行时环境的,比如说堆内存分配上下文环境的初始化等,实际上crt也正是C RunTime的缩写。这也暗示了另外一点:程序并不是从main函数开始执行的,而是从crt中的某个入口开始的,在Linux上此入口是_start。以上Makefile生成的是动态连接的可执行文件,如果要生成静态连接的可执行文件需要将Makefile中的相应段修改:

hw: hw.o
    ld -m elf_i386 -static -o hw /usr/lib/crt1.o /
        /usr/lib/crti.o /
        /usr/lib/gcc/i686-pc-linux-gnu/3.4.6/crtbeginT.o /
        -L/usr/lib/gcc/i686-pc-linux-gnu/3.4.6 /
        -L/usr/i686-pc-linux-gnu/lib /
        -L/usr/lib/ /
        hw.o --start-group -lgcc -lgcc_eh -lc --end-group /
        /usr/lib/gcc/i686-pc-linux-gnu/3.4.6/crtend.o /
        /usr/lib/gcc/i686-pc-linux-gnu/3.4.6/../../../crtn.o


至此,一个可执行文件才最终创建完成。通常的项目中并不需要把编译过程分得如此之细,前三步一般是合为一体的,在Makefile中表现如下:

hw.o: hw.c
    gcc -o hw.o -c hw.c


实际上,如果对hw.c进行了什么更改,那么前三步大多数情况下都是不可避免的。所以把他们写在一起也并没有什么坏处,相反倒可以用--pipe参数告诉编译器用管道替代临时文件,从而提升编译的效率。 

C语言编译过程分成四个步骤

C语言编译过程分成四个步骤: 1,由.c文件到.i文件,这个过程叫预处理 2,由.i文件到.s文件,这个过程叫编译 3,由.s文件到.o文件,这个过程叫汇编 4,由.o文件到可执行文件,这个过...
  • su_buju
  • su_buju
  • 2017-08-13 15:21:31
  • 1392

C++程序编译过程详解

一般来说,我们可以把C++程序编译过程分为以下四步 (1)编译预处理 主要进行源码级别上的操作,预处理器执行源码中的预处理命令(以‘#’号开头的语句),其中预处理命令可以分为以下几类 a. 宏定义命令...
  • AmNotGod
  • AmNotGod
  • 2014-12-08 20:15:48
  • 2740

C/C++——程序实现过程之编译、链接和执行

一个程序的建立经历了预处理、编译、汇编、链接和执行这五个阶段,了解这些过程中所做的工作,对我们在编程时调试程序中有很大帮助。本文总结了编译和链接,阐述了程序建立的过程,希望帮助读者在程序开发中深刻理解...
  • u010757264
  • u010757264
  • 2015-11-28 12:05:30
  • 1453

2. 程序编译的四个阶段

0.引言hello.c(源程序)是一条高级C语言程序,虽然它是以一种易读的形式,让人们容易理解。但是,它却无法直接驱动硬件CPU直接执行。为了我们编写的hello.c程序可以被执行,驱动硬件电路工作,...
  • DylanDong
  • DylanDong
  • 2017-03-05 11:19:25
  • 1856

C/C++ 程序编译与链接的过程详解(静态链接)

我们知道一个程序的执行需要经过编译和链接两个阶段,其过程究竟是怎样的呢? 程序的编译阶段分为以下几个步骤,分别是预编译、编译、汇编、生成二进制可重定向文件(.o)。 预编译: 首先是源代码...
  • xiaobai_aaa
  • xiaobai_aaa
  • 2017-11-02 16:19:45
  • 119

C/C++程序编译步骤以及如何生成可执行文件

一、开篇         许久不碰关于这方面的知识了,前几天同学开课提及到该部分,正好作为回顾吧。         C/C++语言很多人都比较熟悉,这基本上是每位大学生必学的一门编程语言,通常还都是作...
  • qq_21842557
  • qq_21842557
  • 2016-07-12 10:32:32
  • 8440

一个C程序的编译过程(Linux环境下Gcc)

一 以下是C程序一般的编译过程:  从图中看到:      将编写的一个c程序(源代码)转换成可以在硬件上运行的程序(可执行代码),需要进行编译阶段  和链接这两个阶段。      其中,      ...
  • gengyichao
  • gengyichao
  • 2011-06-14 17:03:00
  • 13543

C++ —— C++程序编译的四个过程

C++ —— C++程序编译的四个过程          g++是Linux下C++的编译器;我为什么会选择Linux下的g++编译器,就是因为g++可以看到程序从编译到运行的过程做了些什么。而VS等...
  • a1314521531
  • a1314521531
  • 2016-09-23 10:23:16
  • 642

C/C++程序的编译过程

转自:http://www.ruanyifeng.com/blog/2014/11/compiler.html C/C++源码要运行,首先得转成二进制的机器码。这是编译器的任务。 比如...
  • chenlei0630
  • chenlei0630
  • 2015-03-16 14:51:18
  • 544

C++编译和链接过程的详解

1.基本概念1.编译:编译器对源文件进行编译,就是把源文件中的文本形式存在的源代码翻译成机器语言形式的目标文件的过程,在这个过程中,编译器会进行一系列的语法检查。如果编译通过,就会把对应的CPP转换成...
  • Why000me
  • Why000me
  • 2015-08-16 23:19:21
  • 2124
收藏助手
不良信息举报
您举报文章:C/C++程序编译步骤详解
举报原因:
原因补充:

(最多只允许输入30个字)