C语言的编译与链接 - gcc/ld/ar等工具的介绍

这篇半成品已经在硬盘上放了好久了, 今天终于忍无可忍, 熬夜也要写完(▔皿▔)
起因是因为一个makefile引起的 undefined reference 问题, 下面贴出出错的makefile:

起因:Makefile错误

CC = gcc
CXX = g++
LD = ldd

$(CPP_OBJS): $(DIR_OBJ)/%.o: %.cpp
$(CXX) $(CFLAGS) -o $@ $< $(INCLUDE)

$(OBJS): $(DIR_OBJ)/%.o: %.c
$(CC) -c $(CFLAGS) -o $@ $< $(INCLUDE)

$(APP): $(OBJS) $(CPP_OBJS)
$(CC) $^ -o $(APP) $(LDFLAGS) $(INCLUDE)

这个makefile很简单,g++将.cpp文件编译为 \.o, gcc将 *.c 文件编译为 *.o, 最后 gcc链接所有的 *.o生成可执行程序.
但是上面这个简单的 makefile, 执行make时却报错了:

> gcc   -o _test_code.o _test_code.c -I.
/usr/lib/gcc/x86_64-redhat-linux/4.7.2/../../../../lib64/crt1.o: In function '_start':
(.text+0x20): undefined reference to 'main'
collect2: error: ld returned 1 exit status

错误分析 & 解决

要解释上面的问题, 先来回顾下gcc是如何将*.c编译为可执行程序的, 共4个步骤:

  1. 预编译: gcc -E test.c -o test.i
  2. 生成汇编代码: gcc -S test.i -o test.s
  3. 编译 x.s为 x.o: gcc -c test.s -o test.o 当然 x.c也可以用 -c参数一步编译为 x.o
  4. 链接为可执行文件: gcc test.o -o test

报错很明确的告诉我: lib64/crt1.o 里的_start函数调用了main()函数, 但main()函数缺少定义.
工程中main()函数的定义放在源文件 main.cpp中, 为什么还报 undefined reference to ‘main’呢?
看仔细了, 是在将 test_code.cpp编译为 test_code.o的时候报错, test_code.cpp里没有main()的定义,
这个 makefile的”本意”是这样的: 第一步 g++将 x.cpp文件编译为 x.o, 第二步 gcc将 x.c文件编译为 x.o, 最后 gcc链接所有的 x.o生成可执行程序.
按道理说, 第一步编译只是进行语法分析并生成中间文件, 并不会去找函数有没有定义, 只有在三步链接所有 x.o的时候才可能报出 undefined reference func的错误.
再仔细看看上面的 makefile: $(CXX) $(CFLAGS) -o $@ $< $(INCLUDE)
是没有加 -c参数的, 相当于让 g++一步编译出最终文件(可执行文件), 当然会报 undefined reference 的错误了.
原来是一个笔误🙃

修改也很简单, x.c/x.cpp编译为x.o的过程加上 -c参数就可以了, 正确的 makefile如下:

$(CPP_OBJS): $(DIR_OBJ)/%.o: %.cpp
$(CXX) -c $(CFLAGS) -o $@ $< $(INCLUDE)

$(OBJS): $(DIR_OBJ)/%.o: %.c
$(CC) -c $(CFLAGS) -o $@ $< $(INCLUDE)

$(APP): $(OBJS) $(CPP_OBJS)
$(CC) $^ -o $(APP) $(LDFLAGS) $(INCLUDE)

错误日志里的新发现

再回头看看上面的报错里有一句:

lib64/crt1.o: In function '_start':
(.text+0x20): undefined reference to 'main'

这个 crt1.o是什么? 这个function _start 又是什么🤔?

从上面的打印信息可以知道, gcc先编译出 xxx.o然后再做 Link, 这时候 gcc会对 /usr/lib/crt1.o和我们的 main.o做链接 (因为 crt1.o里的_start函数调用了我们的 main()函数).
由此可见, 可自行文件真正的”入口”并不是 main(), 而是 ctr1.o里的_start, 事实上这个库的名字里 “crt”就是 “startup routine”的意思.
所以, gcc在链接所有的 x.o时, 还会把 /usr/lib/crt1.o也链接进来.
此外gcc有一个默认参数-lc, 表示动态链接 libc库.

扩展阅读: crt1.o,crti.o,crtbegin.o,crtend.o ,crtn.o 与libc.so 的关系 - farmwang的专栏 - CSDN博客

编译过程中的链接: ld

下面是一个分三步编译出 a.out的例子

$ gcc -S main.c -o main.s
$ gcc -c main.s -o main.o
$ gcc main.o -o a.out

我们知道在编译汇编程序时, 也是分compile,link两个步骤:

as hello.s -o hello.o
ld hello.o -o hello

c程序用 gcc链接, 汇编程序用 ld来做链接, 那么这个 ld能不能直接用于 c程序的链接呢?
我们可以试一下, 用 ld去链接所有的 x.o : $(LD) $^ -o $(APP) $(LDFLAGS) $(INCLUDE)
然后make clean && make all, 会报错:
ld: cannot find -lstdc++

@ref (http://sp1.wikidot.com/gnulinker)

附录 I

编译

“编译”的概念: 1、利用编译程序从源语言编写的源程序产生目标程序的过程。 2、用编译程序产生目标程序的动作,编译就是把高级语言变成计算机可以识别的2进制语言。

编译程序把一个源程序翻译成目标程序的工作分为5个阶段:词法分析、语法分析、语义检查和中间代码生成、代码优化、目标代码生成。主要是进行词法分析和语法分析。

链接

链接就是对.o文件进行符号解析和重定位的过程,链接器就是用来完成不同模块之间的链接问题。

  • 符号解析:
    当一个模块使用了在该模块中没有没有定义过的函数或者全局变量时,编译器生成的符号表会标记出所有这样的函数或者全局变量。而连接器的责任就是要到别的模块中去查找它们的定义,如果没有找到适合的定义或者找到的合适定义不唯一,符号解析就无法正常完成。

  • 重定位:
    编译器在编译生成目标文件时,通常都使用从零开始的相对地址。然而,在链接过程中,连接器将从一个指定的地址开始,根据输入的目标文件的顺序以段为单位将它们一个接一个拼接起来。除了目标文件的拼装之外,在重定位的过程中还完成了两个任务:一是生成最终的符号表;二是对代码段的某些位置进行修改,所有需要修改的位置都由编译器生成的重定位表指出。


附录 II

GNU GCC简介:

GNU GCC是一套面向嵌入式领域的交叉编译工具,支持多种编程语言、多种优化选项并且能够支持分步编译、支持多种反汇编方式、支持多种调试信息格式,目前支持X86、ARM7、StrongARM、PPC4XX、MPC8XX、MIPS R3000等多种CPU。
GNU GCC的基本功能包括:

  1. 输出预处理后的C/C++源程序(展开头文件和替换宏)
  2. 输出C/C++源程序的汇编代码
  3. 输出二进制目标文件
  4. 生成静态库
  5. 生成可执行程序
  6. 转换文件格式

GCC 组成:

  • (1) C/C++交叉编译器gcc
    gcc是编译的前端程序,它通过调用其他程序来实现将程序源文件编译成目标文件的功能。
    编译时,它首先调用预处理程序(cpp)对输入的源程序进行处理,然后调用 cc1 将预处理后的程序编译成汇编代码,最后由as将汇编代码编译成目标代码。gcc具有丰富的命令选项,可以控制编译的各个阶段,满足用户的各种编译需求。

  • (2) 汇编器 as
    as将汇编语言程序转换为ELF (Executable and Linking Format,执行时链接文件格式)格式的可重定位目标代码,这些目标代码同其它目标模块或函数库易于定位和链接。
    as产生一个交叉参考表和一个标准的符号表,产生的代码和数据能够放在多个区 (Section)中。

as hello.s -o hello.o  #这一步将汇编源码*.s编译为*.o
ld hello.o -o hello #这一步将*.o链接, 生成ELF
  • (3) 连接器ld
    ld根据链接定位文件Linkcmds中的代码区、数据区、BSS区和栈区等定位信息,将可重定位的目标模块链接成一个单一的、绝对定位的目标程序。该目标程序是ELF格式,并且可以包含调试信息。
    ld会产生一个内存映象文件Map.txt,该文件显示所有目标模块、区和符号的绝对定位地址。它也产生交叉参考列表,显示参考每个全局符号的目标模块。
    ld支持将多个目标模块链接成一个单一的、绝对定位的目标程序,也能够依此对目标模块进行链接,这个特性称为增量链接(Incremental Linking)。
    假如输入文件是一个函数库,ld会自动从函数库装载被其它目标模块参考的函数模块。ld与其它链接程序相比,能提供更有帮助的诊断信息。许多链接器遇到第一个错误即放弃链接,而ld只要有可能都继续执行,帮助用户识别其它错误,有时甚至能获得输出代码。

  • (4) 库管理器ar
    ar将多个可重定位的目标模块归档为一个函数库文件。采用函数库文件,应用程序能够从该文件中自动装载要参考的函数模块,同时将应用程序中频繁调用的函数放入函数库文件中,易于应用程序的开发管理。ar支持ELF格式的函数库文件.

$ gcc -c 1.c 2.c 3.c   # 生成1.o, 2.o, 3.o
$ ar rs lib123.a 1.o 2.o 3.o # 将所有*.o打包成lib123.a:
  • (5) 工程管理器MAKE
    Make是用于自动编译、链接程序的实用工具,使用make后就不需要手工的编译每个程序文件。要使用make,首先要编写makefile。
    Makefile描述程序文件之间的依赖关系,并提供更新文件的命令。在一个程序中,可执行文件依赖于目标文件,而目标文件依赖于源文件。如果makefile文件存在,每次修改完源程序后,用户通常所需要做的事情就是在命令行敲入“make”,然后所有的事情都由make来完成。