文章目录

  • 1 写在前面
  • 2 问题描述
    • 2.1 问题现场
    • 2.2 快速排查
    • 2.3 判断问题
  • 3 知识点突破
    • 3.1 场景复现
    • 3.2 深入分析
    • 3.3 涨点新知识
  • 4 经验总结
  • 5 参考链接
  • 6 更多分享

1 写在前面

最近我们团队在排查一个可能由于GCC链接引发的问题,说起来挺有意思的,在排查的过程中,我不断地调整链接相关的方式,包括 增删链接选项、增删.o文件、调整链接顺序、重组静态库 等等,其中有一次居然给我整出一个 undefined reference to ‘xxx’ 的链接错误;怀疑刨根问底的心态,我们开始这次问题追溯之旅。

通过本文的阅读,你将了解到以下内容:

  • 如何分析并解决 undefined reference to ‘xxx’ 这类链接错误的问题?
  • 如何简单分析静态库文件里面的 符号(函数或全局变量)
  • 如何将object文件打包成静态库?
  • 几个跟链接相关的链接参数的作用。

2 问题描述

2.1 问题现场

我们来看一下问题现场,错误发生在 链接 阶段;如果对GCC编译C程序的流程还不够清晰的,可以再看看我之前写的一篇文章。

【GCC编译优化系列】一文带你了解C代码到底是如何被编译的

链接错误的信息如下图所示:

从图中我们可以看到有大量的 符号(函数或全局变量) 找不到实现体,即我们常说的 undefined reference to 的链接错误。

在此之前,我也写过一篇关于如何快速解决此类 undefined reference to 问题的方法,见:【经验科普】实战分析C工程代码可能遇到的编译问题及其解决思路 第 3.5.1 章节。

2.2 快速排查

根据上面参考文章的解决思路,我们快速过一下:

随便挑一个函数,比如 GAP_ParamsInit 来分析:

它是一个函数,且不是我们实现的函数,是一个厂商SDK提供的静态库中的函数,通过以下方法我们可以确认这一点:

通过grep一搜,发现我们的代码中只有调用的地方而没有实现的地方,然后在一个 libxxx.a 的静态库中提示有这个符号,我们进一步看下:

recan@ubuntu:~$ nm -a lib/libxxx_host.a | grep GAP_ParamsInit
00000001 T GAP_ParamsInit
00000000 t .text.GAP_ParamsInit

可以看到这个函数在静态库里面是带 T 的,这里需要了解点 如何使用nm分析静态库的方法

可以先简单查看以下man:

recan@ubuntu:~$ man nm > xxx.t; cat xxx.t
NM(1)                                                 GNU Development Tools                                                NM(1)NAMEnm - list symbols from object filesSYNOPSISnm [-A|-o|--print-file-name] [-a|--debug-syms][-B|--format=bsd] [-C|--demangle[=style]][-D|--dynamic] [-fformat|--format=format][-g|--extern-only] [-h|--help][-l|--line-numbers] [--inlines][-n|-v|--numeric-sort][-P|--portability] [-p|--no-sort][-r|--reverse-sort] [-S|--print-size][-s|--print-armap] [-t radix|--radix=radix][-u|--undefined-only] [-V|--version][-X 32_64] [--defined-only] [--no-demangle][--plugin name][--no-recurse-limit|--recurse-limit]][--size-sort] [--special-syms][--synthetic] [--with-symbol-versions] [--target=bfdname][objfile...]DESCRIPTIONGNU nm lists the symbols from object files objfile....  If no object files are listed as arguments, nm assumes the filea.out.For each symbol, nm shows:•   The symbol value, in the radix selected by options (see below), or hexadecimal by default.•   The symbol type.  At least the following types are used; others are, as well, depending on the object file format.If lowercase, the symbol is usually local; if uppercase, the symbol is global (external).  There are however a fewlowercase symbols that are shown for special global symbols ("u", "v" and "w")."A" The symbol's value is absolute, and will not be changed by further linking."B""b" The symbol is in the BSS data section.  This section typically contains zero-initialized or uninitialized data,although the exact behavior is system dependent."C" The symbol is common.  Common symbols are uninitialized data.  When linking, multiple common symbols may appearwith the same name.  If the symbol is defined anywhere, the common symbols are treated as undefined references."D""d" The symbol is in the initialized data section."G""g" The symbol is in an initialized data section for small objects.  Some object file formats permit more efficientaccess to small data objects, such as a global int variable as opposed to a large global array."i" For PE format files this indicates that the symbol is in a section specific to the implementation of DLLs.  ForELF format files this indicates that the symbol is an indirect function.  This is a GNU extension to the standardset of ELF symbol types.  It indicates a symbol which if referenced by a relocation does not evaluate to itsaddress, but instead must be invoked at runtime.  The runtime execution will then return the value to be used inthe relocation."I" The symbol is an indirect reference to another symbol."N" The symbol is a debugging symbol."n" The symbol is in the read-only data section."p" The symbol is in a stack unwind section."R""r" The symbol is in a read only data section."S""s" The symbol is in an uninitialized or zero-initialized data section for small objects."T""t" The symbol is in the text (code) section."U" The symbol is undefined."u" The symbol is a unique global symbol.  This is a GNU extension to the standard set of ELF symbol bindings.  Forsuch a symbol the dynamic linker will make sure that in the entire process there is just one symbol with thisname and type in use."V""v" The symbol is a weak object.  When a weak defined symbol is linked with a normal defined symbol, the normaldefined symbol is used with no error.  When a weak undefined symbol is linked and the symbol is not defined, thevalue of the weak symbol becomes zero with no error.  On some systems, uppercase indicates that a default valuehas been specified."W""w" The symbol is a weak symbol that has not been specifically tagged as a weak object symbol.  When a weak definedsymbol is linked with a normal defined symbol, the normal defined symbol is used with no error.  When a weakundefined symbol is linked and the symbol is not defined, the value of the symbol is determined in a system-specific manner without error.  On some systems, uppercase indicates that a default value has been specified."-" The symbol is a stabs symbol in an a.out object file.  In this case, the next values printed are the stabs otherfield, the stabs desc field, and the stab type.  Stabs symbols are used to hold debugging information."?" The symbol type is unknown, or object file format specific.•   The symbol name.OPTIONSThe long and short forms of options, shown here as alternatives, are equivalent.-A-o--print-file-namePrecede each symbol by the name of the input file (or archive member) in which it was found, rather than identifyingthe input file once only, before all of its symbols.-a--debug-symsDisplay all symbols, even debugger-only symbols; normally these are not listed.-B  The same as --format=bsd (for compatibility with the MIPS nm).-C--demangle[=style]Decode (demangle) low-level symbol names into user-level names.  Besides removing any initial underscore prepended bythe system, this makes C++ function names readable. Different compilers have different mangling styles. The optionaldemangling style argument can be used to choose an appropriate demangling style for your compiler.--no-demangleDo not demangle low-level symbol names.  This is the default.--recurse-limit--no-recurse-limit--recursion-limit--no-recursion-limitEnables or disables a limit on the amount of recursion performed whilst demangling strings.  Since the name manglingformats allow for an inifinite level of recursion it is possible to create strings whose decoding will exhaust theamount of stack space available on the host machine, triggering a memory fault.  The limit tries to prevent this fromhappening by restricting recursion to 2048 levels of nesting.The default is for this limit to be enabled, but disabling it may be necessary in order to demangle truly complicatednames.  Note however that if the recursion limit is disabled then stack exhaustion is possible and any bug reportsabout such an event will be rejected.-D--dynamicDisplay the dynamic symbols rather than the normal symbols.  This is only meaningful for dynamic objects, such ascertain types of shared libraries.-f format--format=formatUse the output format format, which can be "bsd", "sysv", or "posix".  The default is "bsd".  Only the firstcharacter of format is significant; it can be either upper or lower case.-g--extern-onlyDisplay only external symbols.-h--helpShow a summary of the options to nm and exit.-l--line-numbersFor each symbol, use debugging information to try to find a filename and line number.  For a defined symbol, look forthe line number of the address of the symbol.  For an undefined symbol, look for the line number of a relocationentry which refers to the symbol.  If line number information can be found, print it after the other symbolinformation.--inlinesWhen option -l is active, if the address belongs to a function that was inlined, then this option causes the sourceinformation for all enclosing scopes back to the first non-inlined function to be printed as well.  For example, if"main" inlines "callee1" which inlines "callee2", and address is from "callee2", the source information for "callee1"and "main" will also be printed.-n-v--numeric-sortSort symbols numerically by their addresses, rather than alphabetically by their names.-p--no-sortDo not bother to sort the symbols in any order; print them in the order encountered.-P--portabilityUse the POSIX.2 standard output format instead of the default format.  Equivalent to -f posix.-r--reverse-sortReverse the order of the sort (whether numeric or alphabetic); let the last come first.-S--print-sizePrint both value and size of defined symbols for the "bsd" output style.  This option has no effect for objectformats that do not record symbol sizes, unless --size-sort is also used in which case a calculated size isdisplayed.-s--print-armapWhen listing symbols from archive members, include the index: a mapping (stored in the archive by ar or ranlib) ofwhich modules contain definitions for which names.-t radix--radix=radixUse radix as the radix for printing the symbol values.  It must be d for decimal, o for octal, or x for hexadecimal.-u--undefined-onlyDisplay only undefined symbols (those external to each object file).-V--versionShow the version number of nm and exit.-X  This option is ignored for compatibility with the AIX version of nm.  It takes one parameter which must be the string32_64.  The default mode of AIX nm corresponds to -X 32, which is not supported by GNU nm.--defined-onlyDisplay only defined symbols for each object file.--plugin nameLoad the plugin called name to add support for extra target types.  This option is only available if the toolchainhas been built with plugin support enabled.If --plugin is not provided, but plugin support has been enabled then nm iterates over the files in${libdir}/bfd-plugins in alphabetic order and the first plugin that claims the object in question is used.Please note that this plugin search directory is not the one used by ld's -plugin option.  In order to make nm usethe  linker plugin it must be copied into the ${libdir}/bfd-plugins directory.  For GCC based compilations the linkerplugin is called liblto_plugin.so.0.0.0.  For Clang based compilations it is called LLVMgold.so.  The GCC plugin isalways backwards compatible with earlier versions, so it is sufficient to just copy the newest one.--size-sortSort symbols by size.  For ELF objects symbol sizes are read from the ELF, for other object types the symbol sizesare computed as the difference between the value of the symbol and the value of the symbol with the next highervalue.  If the "bsd" output format is used the size of the symbol is printed, rather than the value, and -S must beused in order both size and value to be printed.--special-symsDisplay symbols which have a target-specific special meaning.  These symbols are usually used by the target for somespecial processing and are not normally helpful when included in the normal symbol lists.  For example for ARMtargets this option would skip the mapping symbols used to mark transitions between ARM code, THUMB code and data.--syntheticInclude synthetic symbols in the output.  These are special symbols created by the linker for various purposes.  Theyare not shown by default since they are not part of the binary's original source code.--with-symbol-versionsEnables the display of symbol version information if any exists.  The version string is displayed as a suffix to thesymbol name, preceeded by an @ character.  For example foo@VER_1.  If the version is the default version to be usedwhen resolving unversioned references to the symbol then it is displayed as a suffix preceeded by two @ characters.For example foo@@VER_2.--target=bfdnameSpecify an object code format other than your system's default format.@fileRead command-line options from file.  The options read are inserted in place of the original @file option.  If filedoes not exist, or cannot be read, then the option will be treated literally, and not removed.Options in file are separated by whitespace.  A whitespace character may be included in an option by surrounding theentire option in either single or double quotes.  Any character (including a backslash) may be included by prefixingthe character to be included with a backslash.  The file may itself contain additional @file options; any suchoptions will be processed recursively.SEE ALSOar(1), objdump(1), ranlib(1), and the Info entries for binutils.COPYRIGHTCopyright (c) 1991-2020 Free Software Foundation, Inc.Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free DocumentationLicense, Version 1.3 or any later version published by the Free Software Foundation; with no Invariant Sections, with noFront-Cover Texts, and with no Back-Cover Texts.  A copy of the license is included in the section entitled "GNU FreeDocumentation License".binutils-2.34                                              2020-04-07                                                      NM(1)
recan@ubuntu:~$

结合我们的命令输出:

recan@ubuntu:~$ nm -a lib/libxxx_host.a | grep GAP_ParamsInit
00000001 T GAP_ParamsInit
00000000 t .text.GAP_ParamsInit

其中 -a 选项表示导出静态库中的所有符号信息,输出的信息中 Tt 表示该符号位于 text (code) section,即位于代码段,言下之意就是该符号是一个静态库中实现的函数符号。

2.3 判断问题

既然函数符号在静态库中实现了,而我也检查过了,我的链接选项的确已经使用 -lxxx_host 来链接这个静态库,按理说是不应该报 undefined reference to 的,至少也不会这样大批量的函数被报这个错误,况且在我调整链接参数之前,一直都是编译好好的,所以问题的根源就在于 调整的链接参数 上。

带着问题,搜索了一圈,找到一个关键的点:静态库的链接顺序

顺着这个思路,我决定查一查官方资料,我开始去了解 gccman 帮助信息。

方法也很简单,直接Linux命令行下 man gcc即可。

从一大篇的介绍中,我找到了关于使用 -l 指定链接的静态库的相关描述,如下所示:

       -llibrary-l librarySearch the library named library when linking.  (The second alternative with the library as a separate argument isonly for POSIX compliance and is not recommended.)The -l option is passed directly to the linker by GCC.  Refer to your linker documentation for exact details.  Thegeneral description below applies to the GNU linker.The linker searches a standard list of directories for the library.  The directories searched include severalstandard system directories plus any that you specify with -L.Static libraries are archives of object files, and have file names like liblibrary.a.  Some targets also supportshared libraries, which typically have names like liblibrary.so.  If both static and shared libraries are found, thelinker gives preference to linking with the shared library unless the -static option is used.It makes a difference where in the command you write this option; the linker searches and processes libraries andobject files in the order they are specified.  Thus, foo.o -lz bar.o searches library z after file foo.o but beforebar.o.  If bar.o refers to functions in z, those functions may not be loaded.

注意最后一段话,我大致翻译一下:

这个选项在命令行的哪个位置写入结果会有不同,这表现在链接器搜索和处理指定顺序的目标文件上。所以,foo.o -lz bar.o 会先搜索foo.o再搜索z库,然后在搜索bzr.o,如果bar.o有依赖一些z库里面的函数的话,那些函数可能不会被加载(链接)到。

用大白话来说就是:“请注意你链接的顺序,被依赖的需要放在后面!”

3 知识点突破

为了能够很好地突破这个知识点盲区,我准备了一个DEMO工程来实战剖析,工程代码可以从我的 github仓库 中找到。

3.1 场景复现

下面就是我的这个DEMO工程的简要介绍:

└── link_order├── build.sh           编译脚本├── main.c              测试源代码的main.c├── a.c                 a模块的C源文件├── b.c                 b模块的C源文件├── c.c                 c模块的C源文件├── libd.c              d模块的C源文件,打包成静态库参与链接├── libe.c                e模块的C源文件,打包成静态库参与链接├── libf.c                f模块的C源文件,打包成静态库参与链接└── README.md             本说明文档

程序的基本逻辑关系如下描述:

在单静态库的情况下(MUL_LIB_TESt未定义),在程序中,main函数调用a.c的函数,a.c的函数调用b.c的函数,b.c的函数调用c.c的函数,c.c的函数调用libd.c的函数,libd.c的函数最终被打包成静态库参与链接。
在多静态库的情况下(MUL_LIB_TESt定义了),在程序中,main函数调用a.c的函数,a.c的函数调用b.c的函数,b.c的函数调用c.c的函数,c.c的函数调用libd.c的函数,libd.c的函数调用libe.c的函数,libe.c的函数调用libf.c的函数,libf.c的函数调用libd.c的函数,新城互相依赖的关系,libd.c、libe.c、libf.c的函数最终被打包成静态库参与链接。

代码清单如下:

/* main.c */#include <stdio.h>extern void func_a(void);int main(int argc, const char *argv[])
{printf("This is %s in [%s] !\r\n", __func__, __FILE__);func_a();return 0;
}/* a.c */#include <stdio.h>extern void func_b(void);void func_a(void)
{printf("This is %s in [%s] !\r\n", __func__, __FILE__);/* call function b */func_b();
}/* b.c */#include <stdio.h>extern void func_c(void);void func_b(void)
{printf("This is %s in [%s] !\r\n", __func__, __FILE__);/* call function c */func_c();
}/* c.c */#include <stdio.h>extern void func_d(void);void func_c(void)
{printf("This is %s in [%s] !\r\n", __func__, __FILE__);/* call function d */func_d();
}/* libd.c */#include <stdio.h>#ifdef MUL_LIB_TESTextern void func_e(void);void func_d(void)
{printf("This is %s in [%s] (static library) !\r\n", __func__, __FILE__);/* call function e */func_e();
}void func_d_new(void)
{printf("This is %s in [%s] (static library) !\r\n", __func__, __FILE__);
}#elsevoid func_d(void)
{printf("This is %s in [%s] (static library) !\r\n", __func__, __FILE__);
}#endif/* libe.c */#include <stdio.h>extern void func_f(void);void func_e(void)
{printf("This is %s in [%s] (static library) !\r\n", __func__, __FILE__);/* call function f */func_f();
}/* libf.c */#include <stdio.h>extern void func_d_new(void);void func_f(void)
{printf("This is %s in [%s] (static library) !\r\n", __func__, __FILE__);/* call function d new */func_d_new();
}

3.2 深入分析

根据DEMO工程的设定,分为只有一个静态库libd.a的情形和多个静态库相互依赖的情形。

  • 单个静态库链接的情形

在这个情形:工程会生成 a.o/b.o/c.o/libd.a 几个中间文件,最后想生成可执行文件test。

我们试一下编译报错的示例:

link_order$ ./build.sh fail
gcc -c main.c -o main.o -save-temps=obj
gcc -c a.c -o a.o -save-temps=obj
gcc -c b.c -o b.o -save-temps=obj
gcc -c c.c -o c.o -save-temps=obj
gcc -c libd.c -o libd.o -save-temps=obj
ar -rcs libd.a libd.o
gcc -c libe.c -o libe.o -save-temps=obj
ar -rcs libe.a libe.o
gcc -c libf.c -o libf.o -save-temps=obj
ar -rcs libf.a libf.o
gcc a.o b.o main.o -ld -L./ c.o -lc -o test
/usr/bin/ld: c.o: in function `func_c':
c.c:(.text+0x28): undefined reference to `func_d'
collect2: error: ld returned 1 exit status

由于 c.o 中 func_c 依赖了 libd.a中的func_d,而链接的时候,我们的顺序是 a.o b.o libd.o c.o;根据我们之前对GCC的链接静态库的说明翻译可知,这样会导致c.o中找不到func_d的实现,所以就报了 undefined reference to `func_d’

根据这个结论,我们调整一下链接顺序:

link_order$ ./build.sh ok
gcc -c main.c -o main.o -save-temps=obj
gcc -c a.c -o a.o -save-temps=obj
gcc -c b.c -o b.o -save-temps=obj
gcc -c c.c -o c.o -save-temps=obj
gcc -c libd.c -o libd.o -save-temps=obj
ar -rcs libd.a libd.o
gcc -c libe.c -o libe.o -save-temps=obj
ar -rcs libe.a libe.o
gcc -c libf.c -o libf.o -save-temps=obj
ar -rcs libf.a libf.o
gcc a.o b.o main.o c.o -ld -L./ -lc -o testlink_order$ ./test
This is main in [main.c] !
This is func_a in [a.c] !
This is func_b in [b.c] !
This is func_c in [c.c] !
This is func_d in [libd.c] (static library) !

我们可以看到仅仅是把 **libd.a 与 c.o ** 的链接顺序调换一下,问题得到了解决,且执行了正确的输出。

那么有没有什么办法可以做到不需要自己去判断函数的依赖关系,进而再调整链接顺序呢?因为这样的操作在实际的工程操作中,如果代码量庞大,找这种依赖关系可是非常头疼的。

答案肯定是有的,结合下文的情况一并讲解。

  • 多个静态库互相依赖的情形

在这种情形下就比较复杂了:工程会生成 a.o/b.o/c.o/libd.a/libe.a/libf.a 几个中间文件,最后想生成可执行文件test。

与之前的情形不同的是,libd.a/libe.a/libf.a 他们之间有相关依赖的关系,即 libd.a的函数调用libe.a的函数,libe.a的函数调用libf.a的函数,而libf.a的函数又调用libd.a的函数,那么他们的链接顺序该怎么排布,似乎怎么排都不对啊?

我们来试一下,链接报错的情形:

link_order$ ./build.sh mul-lib-fail
gcc -c main.c -o main.o -save-temps=obj
gcc -c a.c -o a.o -save-temps=obj
gcc -c b.c -o b.o -save-temps=obj
gcc -c c.c -o c.o -save-temps=obj
gcc -c libd.c -o libd.o -save-temps=obj
ar -rcs libd.a libd.o
gcc -c libe.c -o libe.o -save-temps=obj
ar -rcs libe.a libe.o
gcc -c libf.c -o libf.o -save-temps=obj
ar -rcs libf.a libf.o
gcc -c libd.c -o libd.o -save-temps=obj -DMUL_LIB_TEST
ar -rcs libd.a libd.o
gcc a.o b.o c.o main.o -lf -ld -le -L./ -lc -o test
/usr/bin/ld: .//libe.a(libe.o): in function `func_e':
libe.c:(.text+0x28): undefined reference to `func_f'
collect2: error: ld returned 1 exit status

从这里可以看到 a.o b.o c.o main.o -lf -ld -le 这样的链接顺序,依然是会报 undefined reference to错误的。

偶然调整,得到一个可以成功链接的方式:

link_order$ ./build.sh mul-lib-ok
gcc -c main.c -o main.o -save-temps=obj
gcc -c a.c -o a.o -save-temps=obj
gcc -c b.c -o b.o -save-temps=obj
gcc -c c.c -o c.o -save-temps=obj
gcc -c libd.c -o libd.o -save-temps=obj
ar -rcs libd.a libd.o
gcc -c libe.c -o libe.o -save-temps=obj
ar -rcs libe.a libe.o
gcc -c libf.c -o libf.o -save-temps=obj
ar -rcs libf.a libf.o
gcc -c libd.c -o libd.o -save-temps=obj -DMUL_LIB_TEST
ar -rcs libd.a libd.o
gcc a.o b.o c.o main.o -ld -le -lf -L./ -lc -o testlink_order$ ./test
This is main in [main.c] !
This is func_a in [a.c] !
This is func_b in [b.c] !
This is func_c in [c.c] !
This is func_d in [libd.c] (static library) !
This is func_e in [libe.c] (static library) !
This is func_f in [libf.c] (static library) !
This is func_d_new in [libd.c] (static library) !

从这里可以看到 a.o b.o c.o main.o -ld -le -lf 这样的链接顺序,是可以连接成功,并跑出正确的输出的。

对比之前,仅仅是 f d e 的静态库顺序变成了 d e f 顺序,到目前其实我 也没想明白到底为什么这样就可以了,有知道的读者可以告知下我。

3.3 涨点新知识

既然上面在多个静态库相互依赖的时候,我都是偶然调整的一个链接顺序才得到正确的链接,那么肯定需要一个稳妥的方法解决,让开发者不必太关系他们之间的链接顺序问题。

查阅资料,我找到了一种方法:

link_order$ ./build.sh mul-lib-fail-solve
gcc -c main.c -o main.o -save-temps=obj
gcc -c a.c -o a.o -save-temps=obj
gcc -c b.c -o b.o -save-temps=obj
gcc -c c.c -o c.o -save-temps=obj
gcc -c libd.c -o libd.o -save-temps=obj
ar -rcs libd.a libd.o
gcc -c libe.c -o libe.o -save-temps=obj
ar -rcs libe.a libe.o
gcc -c libf.c -o libf.o -save-temps=obj
ar -rcs libf.a libf.o
gcc -c libd.c -o libd.o -save-temps=obj -DMUL_LIB_TEST
ar -rcs libd.a libd.o
gcc -Wl,--whole-archive -Wl,--start-group a.o b.o c.o main.o -lf -ld -le -L./ -lc -Wl,--end-group -Wl,-no-whole-archive -o testlink_order$ ./test
This is main in [main.c] !
This is func_a in [a.c] !
This is func_b in [b.c] !
This is func_c in [c.c] !
This is func_d in [libd.c] (static library) !
This is func_e in [libe.c] (static library) !
This is func_f in [libf.c] (static library) !
This is func_d_new in [libd.c] (static library) !

对比之前的链接输入,这里变成了 -Wl,–whole-archive -Wl,–start-group a.o b.o c.o main.o -lf -ld -le -L./ -lc -Wl,–end-group -Wl,-no-whole-archive,里面静态库的顺序还是之前失败时的顺序 f d e;但是在输入的两边加入了 -Wl,–whole-archive -Wl,–start-group-Wl,–end-group -Wl,-no-whole-archive

下面重点介绍一下这几个链接选项:

首先强调一下,这个都是 链接 选项,而 非编译 选项,所以在选项前面会加上 -Wl的前缀,表示这些选项是通过 GCC传递给最终的 链接器(ld) 的。

-Wl,–whole-archive和-Wl,-no-whole-archive:–whole-archive是告诉链接器把后面的目标文件或静态库的所有符号都链接进来,直到遇到 --no-whole-archive,则停止;所以他们两个选项一般都是配套使用;

**-Wl,–start-group 和 -Wl,–end-group **:它们的作用是object目标文件和静态库进行分组归档处理,分组归档之后,就可以重复搜索所有的归档,直到解决所有可能的引用或触发新的未定义引用。 通常,只按命令行中指定的顺序搜索存档一次。 如果需要该存档中的符号来解析稍后在命令行中显示的存档中的对象引用的未定义符号,则链接器将无法解析该引用。 使用此选项会产生显着的性能成本。 最好只在两个或多个档案之间存在不可避免的循环引用时才使用它。

很显然,后面的start-group和end-group正是解决本案例的关键所在;但在实际工程应用中,我们也会常常看到–whole-archive和–start-group一起使用,正如我的DEMO工程那样。

其实我们对ld的man帮助 (man ld) 进行查看,也可以看到原生的帮助信息,感兴趣的可以自行去阅读阅读:

link_order$ man ld | grep "\-\-whole\-archive"--whole-archive, --no-whole-archive, -r, -Ur, --copy-dt-needed-entries, --no-copy-dt-needed-entries, --as-needed,Turn off the effect of the --whole-archive option for subsequent archive files.--whole-archiveFor each archive mentioned on the command line after the --whole-archive option, include every object file in thelink_order$ man ld | grep "\-\-start\-group"gcc -Wl,--start-group foo.o bar.o -Wl,--end-group--start-group archives --end-group

至此,本案例的问题全部解决,请注意这几个有用的链接选项吧。

4 经验总结

  • 善用Linux环境下的man帮助,很多一手的官方帮助信息在man里面都可以得到很好的体现;
  • nm分析静态库符号的方法,最好能熟练掌握,这个用于判断一些跟静态库相关的编译问题还是帮助很大;
  • 一个看似简单的问题,如果用你 已知的 知识解决不了,那么很有可能你已经进入了一个知识盲区,你需要尽可能消灭它;
  • 静态库和.o链接顺序引起的大坑,链接顺序不对的可能性应该要纳入你下一次排查的方向;
  • 打包静态库的方法很简单,只需要使用AR命令操作一下就可以了;
  • 这几个链接选项:-Wl,–whole-archive和-Wl,-no-whole-archive,-Wl,–start-group 和 -Wl,–end-group,有必要掌握一下。

5 参考链接

  • 【GCC编译优化系列】一文带你了解C代码到底是如何被编译的
  • 【经验科普】实战分析C工程代码可能遇到的编译问题及其解决思路
  • GCC link options
  • my workspace with gcc

6 更多分享

架构师李肯

架构师李肯全网同名),一个专注于嵌入式IoT领域的架构师。有着近10年的嵌入式一线开发经验,深耕IoT领域多年,熟知IoT领域的业务发展,深度掌握IoT领域的相关技术栈,包括但不限于主流RTOS内核的实现及其移植、硬件驱动移植开发、网络通讯协议开发、编译构建原理及其实现、底层汇编及编译原理、编译优化及代码重构、主流IoT云平台的对接、嵌入式IoT系统的架构设计等等。拥有多项IoT领域的发明专利,热衷于技术分享,有多年撰写技术博客的经验积累,连续多月获得RT-Thread官方技术社区原创技术博文优秀奖,荣获CSDN博客专家、CSDN物联网领域优质创作者、2021年度CSDN&RT-Thread技术社区之星、2022年RT-Thread全球技术大会讲师、RT-Thread官方嵌入式开源社区认证专家、RT-Thread 2021年度论坛之星TOP4、华为云云享专家(嵌入式物联网架构设计师)等荣誉。坚信【知识改变命运,技术改变世界】!

欢迎关注我的gitee仓库01workstation ,日常分享一些开发笔记和项目实战,欢迎指正问题。

同时也非常欢迎关注我的CSDN主页和专栏:

【CSDN主页-架构师李肯】

【RT-Thread主页-架构师李肯】

【C/C++语言编程专栏】

【GCC专栏】

【信息安全专栏】

【RT-Thread开发笔记】

【freeRTOS开发笔记】

有问题的话,可以跟我讨论,知无不答,谢谢大家。

【GCC编译优化系列】GCC链接失败的错误提示 undefined reference to ‘xxx‘ 可能还有一种情况你没注意到?相关推荐

  1. 【GCC编译优化系列】宏定义名称与函数同名是一种什么骚操作?

    作者简介 *架构师李肯(全网同名)**,一个专注于嵌入式IoT领域的架构师.有着近10年的嵌入式一线开发经验,深耕IoT领域多年,熟知IoT领域的业务发展,深度掌握IoT领域的相关技术栈,包括但不限于 ...

  2. 【GCC编译优化系列】究竟什么样的代码会导致函数调用的栈溢出呢?

    [GCC编译优化系列]究竟什么样的代码会导致函数调用的栈溢出呢? 一段看似铁定栈溢出的函数代码,它一定会溢出吗? 文章目录 1 问题现场 2 简单分析 3 深入分析 3.1 假如不考虑编译优化的情况 ...

  3. GCC编译优化指南【作者:金步国】

    GCC编译优化指南[作者:金步国] GCC编译优化指南 作者:金步国 版权声明 本文作者是一位自由软件爱好者,所以本文虽然不是软件,但是本着 GPL 的精神发布.任何人都可以自由使用.转载.复制和再分 ...

  4. Linux系统下GCC编译错误:“undefined reference to ‘sqrt‘”

    Linux系统下GCC编译错误:"undefined reference to 'sqrt'",解决方法:-lm 数学函数sqrt()位于libm.so库文件中(这些库文件通常位于 ...

  5. Android编译优化系列-kapt篇

    一.背景 本文是编译优化系列文章之 kapt 优化篇,后续还会有 build cache, kotlin, dex 优化等文章,敬请期待.本文由Client Infra->Build Infra ...

  6. Linux下,编译程序遇到“undefined reference to XXX” 报错(可针对webots的编译,不同的文件夹下面不同的cpp,.h文件)

    首先,确保自己安装了一个比较好的编程工具,本人推荐Kdevelop.CLion,程序跳转等都没有问题,可以方便查找问题的所在,同时也可以查看文件路径书写是否出现异常等. 一.确保函数和路径没有任何问题 ...

  7. HiCopyTrader跟单系统启动失败,错误提示:api ms win crt xxxx.dll 之类文件缺失,怎么办

    文章目录 HiCopyTrader 跟单系统启动失败,错误提示:api ms win crt xxxx.dll 之类文件缺失,怎么办 KB2999226 KB3118401 HiCopyTrader ...

  8. GCC 编译 C(C++)静态链接库(gcc -L、gcc -l)和动态链接库(gcc -fPIC -shared)的创建和使用

    1. 库文件 所谓库文件,读者可以将其等价为压缩包文件,该文件内部通常包含不止一个目标文件(也就是二进制文件). 值得一提的是,库文件中每个目标文件存储的代码,并非完整的程序,而是一个个实用的功能模块 ...

  9. GCC编译优化应用预编译头

    服务器编译优化记录 对项目编译优化过程中一些思路和脚本工具实现.对内存受限的编译环境有一些帮助. 工具: https://github.com/wangxiaobai-dd/GccPrecompile ...

最新文章

  1. 关于routerOS设置PPPOE与HOTSPOT并存的说明
  2. iOS 导航栏遮挡视图
  3. Redhat7.2上编译Linux内核源码
  4. Python-OpenCV 处理图像(五):图像中边界和轮廓检测
  5. PMcaff-产品 | 教你做好产品设计规范,提升工作效率
  6. 备考信息系统项目管理师-----Day2
  7. 如何学习Java? 在学习Java的过程中需要掌握哪些技能?
  8. 400分理科学计算机,理科400分,专科批次,有4所211大学可以报考
  9. SpringBoot + Shiro 缓存记住密码
  10. 谷歌招聘主管公开八大求职秘诀
  11. 30条架构原则:助你成为大牛架构师
  12. OPPO Reno7红丝绒新年版开售:精致虎头标识+金色镜头保护圈
  13. 数据结构(三)树、二叉树、最优二叉树
  14. jsp----Session
  15. python的display显示_Python display.display方法代码示例
  16. 山东大学软件工程应用与实践——RIME输入法配置文件分析
  17. 使用Nginx访问日志统计PV和UV
  18. STC51系列单片机不断电自动下载程序
  19. html做成小程序,微信小程序——简单静态网页的制作
  20. Vue路由传参(params 与 query)

热门文章

  1. 高保证音频 HDA(High Definition Audio)
  2. iOS 测试之非代码获取 iPhone 型号及其他信息
  3. 如何通过txt文本编辑一个页面
  4. 如何生成IEEE论文源文件提交的高分辨率PDF?
  5. 在线教育的断崖式崩溃与web3.0的衍生
  6. 简单几步DIY属于自己的IAR工具栏
  7. 求出50-150之间的质数
  8. 您可能不知道(但应该知道)的19个古腾堡编辑器功能
  9. 下载附件下载附件下载附件
  10. 【无标题】mysql增加字段和备注_mysql 修改表名,修改字段类型,增加字段,删除字段,添加字段备注,添加索引...