Toggle navigation
首页
问答
文章
积分商城
专家
专区
更多专区...
文档中心
返回主站
搜索
提问
会员
中心
登录
注册
gcc
链接报错undefined reference to
linker链接报错
【GCC编译优化系列】另类的链接报错undefined reference to
发布于 2022-06-29 21:02:21 浏览:2394
订阅该版
[tocm] 【GCC编译优化系列】GCC链接失败的错误提示 undefined reference to 'xxx' 可能还有一种情况你没注意到? ---- ![image-20220629205735693](https://oss-club.rt-thread.org/uploads/20220714/096bf34b8ea85e218a345f86371ccb2ed8f73236.png) ---- # 1 写在前面 最近我们团队在排查一个可能由于GCC链接引发的问题,说起来挺有意思的,在排查的过程中,我不断地调整链接相关的方式,包括 **增删链接选项、增删.o文件、调整链接顺序、重组静态库** 等等,其中有一次居然给我整出一个 **undefined reference to 'xxx'** 的链接错误;怀疑刨根问底的心态,我们开始这次问题追溯之旅。 通过本文的阅读,你将了解到以下内容: - 如何分析并解决 **undefined reference to 'xxx'** 这类链接错误的问题? - 如何简单分析静态库文件里面的 **符号(函数或全局变量)**? - 如何将object文件打包成静态库? - 几个跟链接相关的链接参数的作用。 # 2 问题描述 ## 2.1 问题现场 我们来看一下问题现场,错误发生在 **链接** 阶段;如果对GCC编译C程序的流程还不够清晰的,可以再看看我之前写的一篇文章。 [【GCC编译优化系列】一文带你了解C代码到底是如何被编译的](https://recan.blog.csdn.net/article/details/121709458) 链接错误的信息如下图所示: ![image-20220627161154227](https://oss-club.rt-thread.org/uploads/20220714/785fce9ee3249b03d48c371b5770b3e2e36acf34.png) 从图中我们可以看到有大量的 **符号(函数或全局变量)** 找不到实现体,即我们常说的 **undefined reference to** 的链接错误。 在此之前,我也写过一篇关于如何快速解决此类 undefined reference to 问题的方法,见:[【经验科普】实战分析C工程代码可能遇到的编译问题及其解决思路](https://recan.blog.csdn.net/article/details/122261137) 第 **3.5.1** 章节。 ## 2.2 快速排查 根据上面参考文章的解决思路,我们快速过一下: ![image-20220627162534920](https://oss-club.rt-thread.org/uploads/20220714/fecbe4cf5907e0797ab621fa35d98b79c4d43c0d.png) 随便挑一个函数,比如 **GAP_ParamsInit** 来分析: 它是一个函数,且不是我们实现的函数,是一个厂商SDK提供的静态库中的函数,通过以下方法我们可以确认这一点: ![image-20220627162948698](https://oss-club.rt-thread.org/uploads/20220714/54eb14706e4989cbf570c4a9e9a230def4d9656b.png) 通过grep一搜,发现我们的代码中只有调用的地方而没有实现的地方,然后在一个 **libxxx.a** 的静态库中提示有这个符号,我们进一步看下: ```shell recan@ubuntu:~$ nm -a lib/libxxx_host.a | grep GAP_ParamsInit 00000001 T GAP_ParamsInit 00000000 t .text.GAP_ParamsInit ``` 可以看到这个函数在静态库里面是带 **T** 的,这里需要了解点 **如何使用nm分析静态库的方法**。 可以先简单查看以下man: ```shell recan@ubuntu:~$ man nm > xxx.t; cat xxx.t NM(1) GNU Development Tools NM(1) NAME nm - list symbols from object files SYNOPSIS nm [-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...] DESCRIPTION GNU nm lists the symbols from object files objfile.... If no object files are listed as arguments, nm assumes the file a.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 few lowercase 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 appear with 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 efficient access 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. For ELF format files this indicates that the symbol is an indirect function. This is a GNU extension to the standard set of ELF symbol types. It indicates a symbol which if referenced by a relocation does not evaluate to its address, but instead must be invoked at runtime. The runtime execution will then return the value to be used in the 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. For such a symbol the dynamic linker will make sure that in the entire process there is just one symbol with this name 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 normal defined symbol is used with no error. When a weak undefined symbol is linked and the symbol is not defined, the value of the weak symbol becomes zero with no error. On some systems, uppercase indicates that a default value has been specified. "W" "w" The symbol is a weak symbol that has not been specifically tagged as a weak object symbol. When a weak defined symbol is linked with a normal defined symbol, the normal defined symbol is used with no error. When a weak undefined 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 other field, 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. OPTIONS The long and short forms of options, shown here as alternatives, are equivalent. -A -o --print-file-name Precede each symbol by the name of the input file (or archive member) in which it was found, rather than identifying the input file once only, before all of its symbols. -a --debug-syms Display 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 by the system, this makes C++ function names readable. Different compilers have different mangling styles. The optional demangling style argument can be used to choose an appropriate demangling style for your compiler. --no-demangle Do not demangle low-level symbol names. This is the default. --recurse-limit --no-recurse-limit --recursion-limit --no-recursion-limit Enables or disables a limit on the amount of recursion performed whilst demangling strings. Since the name mangling formats allow for an inifinite level of recursion it is possible to create strings whose decoding will exhaust the amount of stack space available on the host machine, triggering a memory fault. The limit tries to prevent this from happening 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 complicated names. Note however that if the recursion limit is disabled then stack exhaustion is possible and any bug reports about such an event will be rejected. -D --dynamic Display the dynamic symbols rather than the normal symbols. This is only meaningful for dynamic objects, such as certain types of shared libraries. -f format --format=format Use the output format format, which can be "bsd", "sysv", or "posix". The default is "bsd". Only the first character of format is significant; it can be either upper or lower case. -g --extern-only Display only external symbols. -h --help Show a summary of the options to nm and exit. -l --line-numbers For each symbol, use debugging information to try to find a filename and line number. For a defined symbol, look for the line number of the address of the symbol. For an undefined symbol, look for the line number of a relocation entry which refers to the symbol. If line number information can be found, print it after the other symbol information. --inlines When option -l is active, if the address belongs to a function that was inlined, then this option causes the source information 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-sort Sort symbols numerically by their addresses, rather than alphabetically by their names. -p --no-sort Do not bother to sort the symbols in any order; print them in the order encountered. -P --portability Use the POSIX.2 standard output format instead of the default format. Equivalent to -f posix. -r --reverse-sort Reverse the order of the sort (whether numeric or alphabetic); let the last come first. -S --print-size Print both value and size of defined symbols for the "bsd" output style. This option has no effect for object formats that do not record symbol sizes, unless --size-sort is also used in which case a calculated size is displayed. -s --print-armap When listing symbols from archive members, include the index: a mapping (stored in the archive by ar or ranlib) of which modules contain definitions for which names. -t radix --radix=radix Use radix as the radix for printing the symbol values. It must be d for decimal, o for octal, or x for hexadecimal. -u --undefined-only Display only undefined symbols (those external to each object file). -V --version Show 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 string 32_64. The default mode of AIX nm corresponds to -X 32, which is not supported by GNU nm. --defined-only Display only defined symbols for each object file. --plugin name Load the plugin called name to add support for extra target types. This option is only available if the toolchain has 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 use the linker plugin it must be copied into the ${libdir}/bfd-plugins directory. For GCC based compilations the linker plugin is called liblto_plugin.so.0.0.0. For Clang based compilations it is called LLVMgold.so. The GCC plugin is always backwards compatible with earlier versions, so it is sufficient to just copy the newest one. --size-sort Sort symbols by size. For ELF objects symbol sizes are read from the ELF, for other object types the symbol sizes are computed as the difference between the value of the symbol and the value of the symbol with the next higher value. If the "bsd" output format is used the size of the symbol is printed, rather than the value, and -S must be used in order both size and value to be printed. --special-syms Display symbols which have a target-specific special meaning. These symbols are usually used by the target for some special processing and are not normally helpful when included in the normal symbol lists. For example for ARM targets this option would skip the mapping symbols used to mark transitions between ARM code, THUMB code and data. --synthetic Include synthetic symbols in the output. These are special symbols created by the linker for various purposes. They are not shown by default since they are not part of the binary's original source code. --with-symbol-versions Enables the display of symbol version information if any exists. The version string is displayed as a suffix to the symbol name, preceeded by an @ character. For example foo@VER_1. If the version is the default version to be used when resolving unversioned references to the symbol then it is displayed as a suffix preceeded by two @ characters. For example foo@@VER_2. --target=bfdname Specify an object code format other than your system's default format. @file Read command-line options from file. The options read are inserted in place of the original @file option. If file does 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 the entire option in either single or double quotes. Any character (including a backslash) may be included by prefixing the character to be included with a backslash. The file may itself contain additional @file options; any such options will be processed recursively. SEE ALSO ar(1), objdump(1), ranlib(1), and the Info entries for binutils. COPYRIGHT Copyright (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 Documentation License, Version 1.3 or any later version published by the Free Software Foundation; with no Invariant Sections, with no Front-Cover Texts, and with no Back-Cover Texts. A copy of the license is included in the section entitled "GNU Free Documentation 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** 选项表示导出静态库中的所有符号信息,输出的信息中 **T** 和 **t** 表示该符号位于 **text (code) section**,即位于代码段,言下之意就是该符号是一个静态库中实现的函数符号。 ## 2.3 判断问题 既然函数符号在静态库中实现了,而我也检查过了,我的链接选项的确已经使用 **-lxxx_host** 来链接这个静态库,按理说是不应该报 **undefined reference to** 的,至少也不会这样大批量的函数被报这个错误,况且在我调整链接参数之前,一直都是编译好好的,所以问题的根源就在于 **调整的链接参数** 上。 带着问题,搜索了一圈,找到一个关键的点:**静态库的链接顺序** ![image-20220627173556698](https://oss-club.rt-thread.org/uploads/20220714/ad903af5ef92e4fe1e9671c1c282fdfc2eed5eed.png) 顺着这个思路,我决定查一查官方资料,我开始去了解 **gcc** 的 **man** 帮助信息。 方法也很简单,直接Linux命令行下 `man gcc`即可。 从一大篇的介绍中,我找到了关于使用 **-l** 指定链接的静态库的相关描述,如下所示: ```shell -llibrary -l library Search the library named library when linking. (The second alternative with the library as a separate argument is only 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. The general description below applies to the GNU linker. The linker searches a standard list of directories for the library. The directories searched include several standard 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 support shared libraries, which typically have names like liblibrary.so. If both static and shared libraries are found, the linker 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 and object files in the order they are specified. Thus, foo.o -lz bar.o searches library z after file foo.o but before bar.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仓库](https://github.com/recan-li/coding-01workstation/tree/master/workspace/gcc/link_order) 中找到。 ## 3.1 场景复现 下面就是我的这个DEMO工程的简要介绍: ```shell └── 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的函数最终被打包成静态库参与链接。 代码清单如下: ```c /* main.c */ #include
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
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
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
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
#ifdef MUL_LIB_TEST extern 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__); } #else void func_d(void) { printf("This is %s in [%s] (static library) !\r\n", __func__, __FILE__); } #endif /* libe.c */ #include
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
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。 我们试一下编译报错的示例: ```shell 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'**。 根据这个结论,我们调整一下链接顺序: ```shell 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 test link_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的函数**,那么他们的链接顺序该怎么排布,似乎怎么排都不对啊? 我们来试一下,链接报错的情形: ```shell 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**错误的。 偶然调整,得到一个可以成功链接的方式: ```shell 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 test link_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 涨点新知识 既然上面在多个静态库相互依赖的时候,我都是偶然调整的一个链接顺序才得到正确的链接,那么肯定需要一个稳妥的方法解决,让开发者不必太关系他们之间的链接顺序问题。 查阅资料,我找到了一种方法: ```shell 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 test link_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)** 进行查看,也可以看到原生的帮助信息,感兴趣的可以自行去阅读阅读: ```shell 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-archive For each archive mentioned on the command line after the --whole-archive option, include every object file in the link_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代码到底是如何被编译的](https://recan.blog.csdn.net/article/details/121709458) - [【经验科普】实战分析C工程代码可能遇到的编译问题及其解决思路](https://recan.blog.csdn.net/article/details/122261137) - [GCC link options](https://gcc.gnu.org/onlinedocs/gcc/Link-Options.html) - [my workspace with gcc](https://github.com/recan-li/coding-01workstation/tree/master/workspace/gcc) # 6 更多分享 > **[架构师李肯](https://recan.blog.csdn.net)** > > **架构师李肯**(**全网同名**),一个专注于嵌入式IoT领域的架构师。有着近10年的嵌入式一线开发经验,深耕IoT领域多年,熟知IoT领域的业务发展,深度掌握IoT领域的相关技术栈,包括但不限于主流RTOS内核的实现及其移植、硬件驱动移植开发、网络通讯协议开发、编译构建原理及其实现、底层汇编及编译原理、编译优化及代码重构、主流IoT云平台的对接、嵌入式IoT系统的架构设计等等。拥有多项IoT领域的发明专利,热衷于技术分享,有多年撰写技术博客的经验积累,连续多月获得RT-Thread官方技术社区原创技术博文优秀奖,荣获[CSDN博客专家](https://recan.blog.csdn.net/?type=blog)、[CSDN物联网领域优质创作者](http://yyds.recan-li.cn)、[2021年度CSDN&RT-Thread技术社区之星](https://blog.csdn.net/szullc/article/details/123860472)、[2022年RT-Thread全球技术大会讲师](https://club.rt-thread.org/ask/article/afa56894c113369a.html)、[RT-Thread官方嵌入式开源社区认证专家](https://club.rt-thread.org/ask/experts.html)、[RT-Thread 2021年度论坛之星TOP4](https://club.rt-thread.org/ask/article/3317.html)、[华为云云享专家(嵌入式物联网架构设计师)](https://bbs.huaweicloud.com/community/usersnew/id_1573655458316259)等荣誉。坚信【知识改变命运,技术改变世界】! > --- 欢迎关注我的[gitee仓库01workstation](https://gitee.com/recan-li/coding-01workstation) ,日常分享一些开发笔记和项目实战,欢迎指正问题。 同时也非常欢迎关注我的CSDN主页和专栏: [【CSDN主页-架构师李肯】](http://yyds.recan-li.cn) [【RT-Thread主页-架构师李肯】](https://club.rt-thread.org/u/18001) [【C/C++语言编程专栏】](https://blog.csdn.net/szullc/category_8450784.html) [【GCC专栏】](https://blog.csdn.net/szullc/category_8626555.html) [【信息安全专栏】](https://blog.csdn.net/szullc/category_8452787.html) [【RT-Thread开发笔记】](https://blog.csdn.net/szullc/category_11461616.html) [【freeRTOS开发笔记】](https://blog.csdn.net/szullc/category_11467856.html) 有问题的话,可以跟我讨论,知无不答,谢谢大家。
10
条评论
默认排序
按发布时间排序
登录
注册新账号
关于作者
李肯陪你玩赚嵌入式
2022年度和2023年度RT-Thread社区优秀开源布道师,COC深圳城市开发者社区主理人,专注于嵌入式物联网的架构设计
文章
47
回答
504
被采纳
82
关注TA
发私信
相关文章
1
gcc编译不能链接超过 2MB
2
Studio默认的工具链为什么那么老,支持用新的吗?
3
studio 字节对齐问题求解
4
BSP(zynq7000)例程是否没有做过gcc 9的编译测试?
5
环境搭建问题和AT软件包的问题
6
arm-none-eabi-gcc下编译utest变量未定义
7
rtconfig.py 下编译标志解释
8
关于GCC和ARMCC编译后生成BIN,文件的大小
9
gcc环境 libc 使用, code size 优化请教
10
rt-thread使用win32API
推荐文章
1
RT-Thread应用项目汇总
2
玩转RT-Thread系列教程
3
国产MCU移植系列教程汇总,欢迎查看!
4
机器人操作系统 (ROS2) 和 RT-Thread 通信
5
五分钟玩转RT-Thread新社区
6
【技术三千问】之《玩转ART-Pi》,看这篇就够了!干货汇总
7
关于STM32H7开发板上使用SDIO接口驱动SD卡挂载文件系统的问题总结
8
STM32的“GPU”——DMA2D实例详解
9
RT-Thread隐藏的宝藏之completion
10
【ART-PI】RT-Thread 开启RTC 与 Alarm组件
热门标签
RT-Thread Studio
串口
Env
LWIP
SPI
AT
Bootloader
Hardfault
CAN总线
FinSH
ART-Pi
USB
DMA
文件系统
RT-Thread
SCons
RT-Thread Nano
线程
MQTT
STM32
RTC
FAL
rt-smart
ESP8266
I2C_IIC
UART
WIZnet_W5500
ota在线升级
freemodbus
PWM
flash
cubemx
packages_软件包
BSP
潘多拉开发板_Pandora
定时器
ADC
flashDB
GD32
socket
中断
编译报错
Debug
SFUD
rt_mq_消息队列_msg_queue
msh
keil_MDK
ulog
C++_cpp
MicroPython
本月问答贡献
a1012112796
10
个答案
1
次被采纳
踩姑娘的小蘑菇
4
个答案
1
次被采纳
红枫
4
个答案
1
次被采纳
张世争
4
个答案
1
次被采纳
Ryan_CW
4
个答案
1
次被采纳
本月文章贡献
catcatbing
3
篇文章
5
次点赞
YZRD
2
篇文章
5
次点赞
qq1078249029
2
篇文章
2
次点赞
xnosky
2
篇文章
1
次点赞
Woshizhapuren
1
篇文章
5
次点赞
回到
顶部
发布
问题
投诉
建议
回到
底部