使用MDK编译RT-Thread(LUA与DFS组件都打开)时的出现符号重复定义的问题的研究与解决
笔者最近使用MDK编译RT-Thread,使能LUA组件和DFS组件时,并使能Microlib后,编译出现如下错误:
.\build\rtthread-stm32f4xx.axf: Error: L6200E: Symbol rename multiply defined (by iostubs.o and dfs_posix.o).
而不开启LUA,开启DFS,则编译正确,并且生成的固件中使用的rename函数将是DFS中的rename;反之开启LUA,不开启DFS,同样可以正确编译。
且看笔者的分析:
armlink的链接选项--verbose,可以从--list的输出中得到详细的信息。下文给出的map文件也都是开启此选项生成的。
' --verbose --list rtthread-stm32.map'
标准库与微库(Microlib)
微型库的介绍可以从MDK的help手册中找到。简而言之,它是针对资源紧张的嵌入式设备专门优化微型标准库。在默认情况下,连接器将链接标准库。
添加编译选项'--library_type=microlib',连接器将使用微库链接程序。
LUA中使用了fopen函数,这是笔者搜索LUA的源码文件得到的信息。
$ find -name "*.[ch]" -exec grep -nh "fopen" {} \; -print
618: lf.f = fopen(filename, "r");
./lua/lauxlib.c
190: *pf = fopen(filename, mode);
230: *pf = fopen(filename, mode);
282: *pf = fopen(filename, "r");
./lua/liolib.c
333: FILE *f = fopen(filename, "r"); /* try to open file */
./lua/loadlib.c
首先使用标准库编译。成功编译后,打开map文件,因为_sys_open是fopen所依赖的底层IO函数,因此我们搜索_sys_open,可以找到如下数据:
Loading member sys_io.o from c_w.l.
definition: __stderr_name
definition: __stdin_name
definition: __stdout_name
definition: _sys_open
definition: _sys_close
definition: _sys_write
definition: _sys_read
definition: _sys_istty
definition: _sys_seek
definition: _sys_ensure
definition: _sys_flen
reference : __I$use$semihosting
reference : __semihosting_library_function
reference : __semihosting_library_function
reference : __semihosting_library_function
reference : __semihosting_library_function
reference : __semihosting_library_function
reference : __semihosting_library_function
reference : __semihosting_library_function
reference : strlen
可以看出_sys_open这个函数是在sys_io.o里定义,上面这个段落里列出了sys_io.o里定义的所有符号(以definition开始的行),以及引用的所有符号(以reference开始的行)。
我们可以自己实现一个mysysio.c, 重新定义这些符号,那么编译器就会链接我们的符号而忽略sys_io.o中的符号。读者请务必注意,如果你的mysysio.c里定义的符号不全,比如你漏掉了 __stdout_name,那么armlink(MDK的底层连接器)会从sys_io.o里加载__stdout_name符号,并同时将sys_io.o里的所有符号都加载,这会导致与mysysio中其他符号重复。即会得到如下错误:
xxxxx.axf: Error: L6200E: Symbol _sys_open multiply defined (by sys_io.o and mysysio.o).
要真正解决这个错误,方法就是要将sys_io.o的所有符号全部重新实现。
不过,map文件只是给出了这些符号的名称,在C语言中,函数名、变量名、goto标记等都是符号,所以我们需要参考相应的头文件才能知道这些符号到底是什么。
sys_io.o中所有定义的符号可在 rt_sys.h找找到,它位于X:\Keil\ARM\RV31\INC\目录下。
现在我们使用Microlib链接,即添加编译选项 --library_type=microlib
并生成map文件,
Loading member iostubs.o from mc_w.l.
definition: clearerr
definition: fclose
definition: feof
definition: ferror
definition: fflush
definition: fgetpos
definition: fopen
definition: freopen
definition: fseek
definition: fsetpos
definition: ftell
definition: remove
definition: rename
definition: rewind
definition: setbuf
definition: setvbuf
definition: tmpfile
definition: tmpnam
可以看出,此时 fopen符号是在 iostubs里定义的。如果我们编写的程序引用了上一段中的任意一个符号,根据前面的知识,这样意味着下面的所有符号都会被导入。如果此时你的应用程序自己实现了一个rename函数,那么就会出现重复符号的错误。要想解决这个问题。要么自己将iostub.o里的所有符号全部实现,要么将自己的应用程序的符号实现全部移除,这样连接器就会使用iostubs里的符号。
需要说明的是,如果应用程序没有引用上述任意一个符号,则iosubs.o就不会被链接入最终的elf文件中。
现在来我们就知道开头的问题的原因了:
LUA里使用了fopen等函数,因为RTT源码里没有提供这个函数实现,armlink会从库中加载这个符号,发现这个符号位于iostubs.o中,然后会将iostubs中的所有符号都链入,这其中包括rename符号,而DFS中有rename实现,所有出现重复定义。
而取消LUA后,程序中就没有对fopen家族的引用了,并且也没iostubs.o中其他符号的引用,那么iostubs.o不会被链入,而DFS中的rename会可以被链接入最终的ELF文件中。
总结:
只有当一个.o库文件里的所有符号都被用户程序中重新实现后,这个库.o文件就不会被引用。链接器就会使用用户的实现。如果用户符号定义不全,就会导致在链接时出现符号重复定义的错误。
PS:我之前一直以为库里的符号都是弱符号,都可以被用户函数实现覆盖,现在来看不是。有可能是链接器进行判断,如果缺少某一个符号,就链接包含该符号的.o文件
参考文献:
[1].MDK 帮助手册
MDK-ARM Primer
Configure Cortex-M Target
Library Retarget File
Libraries and Floating Point Support Guide
Direct semihosting C library function dependencies
[2].
.\build\rtthread-stm32f4xx.axf: Error: L6200E: Symbol rename multiply defined (by iostubs.o and dfs_posix.o).
而不开启LUA,开启DFS,则编译正确,并且生成的固件中使用的rename函数将是DFS中的rename;反之开启LUA,不开启DFS,同样可以正确编译。
且看笔者的分析:
armlink的链接选项--verbose,可以从--list的输出中得到详细的信息。下文给出的map文件也都是开启此选项生成的。
' --verbose --list rtthread-stm32.map'
标准库与微库(Microlib)
微型库的介绍可以从MDK的help手册中找到。简而言之,它是针对资源紧张的嵌入式设备专门优化微型标准库。在默认情况下,连接器将链接标准库。
添加编译选项'--library_type=microlib',连接器将使用微库链接程序。
LUA中使用了fopen函数,这是笔者搜索LUA的源码文件得到的信息。
$ find -name "*.[ch]" -exec grep -nh "fopen" {} \; -print
618: lf.f = fopen(filename, "r");
./lua/lauxlib.c
190: *pf = fopen(filename, mode);
230: *pf = fopen(filename, mode);
282: *pf = fopen(filename, "r");
./lua/liolib.c
333: FILE *f = fopen(filename, "r"); /* try to open file */
./lua/loadlib.c
首先使用标准库编译。成功编译后,打开map文件,因为_sys_open是fopen所依赖的底层IO函数,因此我们搜索_sys_open,可以找到如下数据:
Loading member sys_io.o from c_w.l.
definition: __stderr_name
definition: __stdin_name
definition: __stdout_name
definition: _sys_open
definition: _sys_close
definition: _sys_write
definition: _sys_read
definition: _sys_istty
definition: _sys_seek
definition: _sys_ensure
definition: _sys_flen
reference : __I$use$semihosting
reference : __semihosting_library_function
reference : __semihosting_library_function
reference : __semihosting_library_function
reference : __semihosting_library_function
reference : __semihosting_library_function
reference : __semihosting_library_function
reference : __semihosting_library_function
reference : strlen
可以看出_sys_open这个函数是在sys_io.o里定义,上面这个段落里列出了sys_io.o里定义的所有符号(以definition开始的行),以及引用的所有符号(以reference开始的行)。
我们可以自己实现一个mysysio.c, 重新定义这些符号,那么编译器就会链接我们的符号而忽略sys_io.o中的符号。读者请务必注意,如果你的mysysio.c里定义的符号不全,比如你漏掉了 __stdout_name,那么armlink(MDK的底层连接器)会从sys_io.o里加载__stdout_name符号,并同时将sys_io.o里的所有符号都加载,这会导致与mysysio中其他符号重复。即会得到如下错误:
xxxxx.axf: Error: L6200E: Symbol _sys_open multiply defined (by sys_io.o and mysysio.o).
要真正解决这个错误,方法就是要将sys_io.o的所有符号全部重新实现。
不过,map文件只是给出了这些符号的名称,在C语言中,函数名、变量名、goto标记等都是符号,所以我们需要参考相应的头文件才能知道这些符号到底是什么。
sys_io.o中所有定义的符号可在 rt_sys.h找找到,它位于X:\Keil\ARM\RV31\INC\目录下。
现在我们使用Microlib链接,即添加编译选项 --library_type=microlib
并生成map文件,
Loading member iostubs.o from mc_w.l.
definition: clearerr
definition: fclose
definition: feof
definition: ferror
definition: fflush
definition: fgetpos
definition: fopen
definition: freopen
definition: fseek
definition: fsetpos
definition: ftell
definition: remove
definition: rename
definition: rewind
definition: setbuf
definition: setvbuf
definition: tmpfile
definition: tmpnam
可以看出,此时 fopen符号是在 iostubs里定义的。如果我们编写的程序引用了上一段中的任意一个符号,根据前面的知识,这样意味着下面的所有符号都会被导入。如果此时你的应用程序自己实现了一个rename函数,那么就会出现重复符号的错误。要想解决这个问题。要么自己将iostub.o里的所有符号全部实现,要么将自己的应用程序的符号实现全部移除,这样连接器就会使用iostubs里的符号。
需要说明的是,如果应用程序没有引用上述任意一个符号,则iosubs.o就不会被链接入最终的elf文件中。
现在来我们就知道开头的问题的原因了:
LUA里使用了fopen等函数,因为RTT源码里没有提供这个函数实现,armlink会从库中加载这个符号,发现这个符号位于iostubs.o中,然后会将iostubs中的所有符号都链入,这其中包括rename符号,而DFS中有rename实现,所有出现重复定义。
而取消LUA后,程序中就没有对fopen家族的引用了,并且也没iostubs.o中其他符号的引用,那么iostubs.o不会被链入,而DFS中的rename会可以被链接入最终的ELF文件中。
总结:
只有当一个.o库文件里的所有符号都被用户程序中重新实现后,这个库.o文件就不会被引用。链接器就会使用用户的实现。如果用户符号定义不全,就会导致在链接时出现符号重复定义的错误。
PS:我之前一直以为库里的符号都是弱符号,都可以被用户函数实现覆盖,现在来看不是。有可能是链接器进行判断,如果缺少某一个符号,就链接包含该符号的.o文件
参考文献:
[1].MDK 帮助手册
MDK-ARM Primer
Configure Cortex-M Target
Library Retarget File
Libraries and Floating Point Support Guide
Direct semihosting C library function dependencies
[2].
还没人转发这篇日记