经验科普实战分析C工程代码可能遇到的编译问题及其解决思路

网友投稿 644 2022-05-29

1 前言

月初的时候我整理发出的技术文章:《【gcc编译优化系列】一文带你了解C代码到底是如何被编译的》,已经收到了好几个朋友的了;同时,特别惊喜的是,还收到论坛元老级别的大佬 aozima推荐和打赏,真的倍感荣幸。

这也从侧面证实了,我选择的这个【C代码编译】话题的确是不少人遇到的困惑,而我写的关于编译问题的文章也正好体现它的价值,所以我也想着尽快把C代码编译的第二篇文章输出来了,也真心希望这份实战分析能帮助到更多人解决他们遇到的各式各样的编译问题。

事与愿违,最近事多,一拖稿就是3个星期,这不,直到现在(再过几个小时就2022了)才发出来。

【经验科普】实战分析C工程代码可能遇到的编译问题及其解决思路

2 回顾

2.1 主要内容

上一篇文章我也交代过,第一篇文章侧重于结合gcc编译器把C代码编译的整个流程介绍清楚,第二章文章会侧重于结合真实的代码场景分析遇到什么样的编译问题应该怎么样去解决。

一句话:上篇注重理论基础,本篇注重实战演练 !

2.2 知识点回顾

再次强调下第一篇的理论基础:

C代码编译的步骤,需要经历预编译、编译、汇编、链接等几个关键步骤,最后才能生成二进制文件,而这个二进制文件就是能被CPU识别并正确执行指令的唯一凭证。

我重新补画了一张图,它基本覆盖了一个C工程代码在开发阶段的生命周期,本次的实战演练也是主要围绕着这张图展开。

值得注意的是,本文所提及的【编译问题】主要在2/3/4/5/6这几个环节。

3 实战分析

下面我会就每个阶段,结合案例指出需要注意的事项或者分析会遇到哪类问题,以及这类问题应该如何去解决。

注意:下文中截取的代码案例均来自RTT技术论坛。

3.1 代码编写阶段

在代码编写阶段,其实也没多少好说的,但还是有人会遇到编译出错的问题,为什么呢?

这里提几点经验之谈:

安装IDE软件或开发工具,亦或建立workspace,尽量尽量尽量不要用中文,哪怕英文差一些,搞个拼音也好一些;

如果是在Windows下编写、编译代码,强烈建议不要搞太深的目录,你的workspace-path尽量路径短一些,不能保证每个编译器对这种长路劲目录都支持得很好;

代码注释也尽量别中文,因中文编码问题导致的代码文件乱码之后狼狈不堪的例子真的太多了;如果一定要用中文,大家一定要约定好编码格式,GBK还是UTF-8,这里建议用UTF-8,关于它们的区别与联系,推荐阅读。

代码编写风格尽量遵循规范,你的开发团队要求怎么样就怎么样,这种没有优劣之分,有了规范就是需要遵守。

随手抓几个论坛中在代码编写阶段遇到编译问题的帖子看看:

Windows命令行限制导致的编译问题:本质还是上面提及的路径不用太长,目录不要太深;在Windows下可以把命令行的内容先写入一个文本文件中转下,绕开这个问题,很多SDK的编译流程在Windows下就是这么玩的,比较麻烦。

中文编码问题:本质还是少写中文注释,哪怕蹩脚一点的英文,这不还有翻译软件吗?确保语法没问题就行了。

中文编码相关的问题,还不少勒:

代码编写规范的问题:大家约定好了,就一起共同遵守吧!

3.2 预编译阶段

预编译阶段要做的事情,参考我的上一篇文章,这里重点介绍几种非常容易在预编译阶段遇到的编译问题。

为了更好地展示编译错误,我在下面提示编译错误的时候,都采用英文为主辅以中文的方式来描述。

3.2.1 No such file or directory (找不到某个文件或目录)

这里举几个典型的例子:

例子1:问题的根源,头文件未包含,可能还包含头文件嵌套包含的问题,比如头文件A包含了头文件B,报的是没找到头文件B,那么include头文件A就能解决;

例子2:问题还是出在include上面,这个头文件也的确是存在的,但是编译器报找不到这个头文件,原因在于头文件所在的目录没有在编译器检索头文件的列表里面,这个检索列表一般包括:系统级别的include目录(linux下/usr/include、/usr/local/include这些),编译器所在include目录,最后就是用户自定义目录。指定这个目录,有不同的方法,以gcc编译为例,是需要在CFLAGS里面添加**-Ixxx**(I是大写的,xxx可以是相对路径也可以是绝对路径),具体可以参考下这篇文章。另外提一点,如果你的工程中有同名的头文件,一定要注意他们被搜索的顺序,这是决定你的代码究竟include哪个头文件的唯一参考。

例子3:这个问题的本质还是头文件查找的问题。

例子4:这个本质也是头文件查找路劲的问题,楼主通过把头文件拷贝到指定路劲解决了的问题,但比较好的解决方案,我认为是将原头文件的路径添加进头文件检索的路径比较好。

总结一下:出现No such file or directory的时候重点排查几个方面:

先确认提示的这个文件是否真的不存在?如果文件存在,往下排查;

如果文件是头文件,那么首先应该考虑的是头文件所在的目录是否在编译器头文件检索的列表里面?

如果文件是C文件,则需要排查下Makefile或CMake或sconc的配置对该C文件的添加情况。

3.2.2 宏定义的问题

在C代码中,可以说宏定义真的是太常见了,可以说是无处不在。

可以毫不客气地说,没有哪一个号称精通C语言的大神玩宏定义玩得不6的。

在宏定义中,经常遇到的编译问题就是,宏定义是如何展开的?

宏定义展开之后,对上下文代码编译的影响是什么?

搞清楚这个,解决因宏定义引发的编译问题就一点都不难了。

欲知,如何查看宏定义展开后的代码原型,你可以尝试跳到 4.2.2 章节提前了解。

另外,还有一种宏定义报错比较常见的就是某个宏定义定义在a.h头文件中,但是引用该头文件的C文件没有包括该头文件,导致编译报错;这种错误,本质是一个3.2.1提到的头文件包含问题。

下面就宏定义编译报错的问题,截取论坛中比较典型的错误:

例子1:错误的宏定义导致编译失败。

例子2:这个本质还是编译器宏定义的问题,引发的头文件包含一系列的问题。

例子3:头文件中缺少GCC宏定义的判断,导致有些头文件或宏定义没有定义,进而编译报错。

另外,在论坛的问题里面检索宏定义的时候,发现了这么一个**问题**,留给大家讨论讨论。

3.2.3 条件编译的问题

常用的条件编译有:

#if 一般后面接一个宏定义表达式,当表达式的值为非0时,#if后面的代码参与编译,否则就不参与;

#else 与**#if**类似;

#elif 这是条件编译的嵌套写法,支持多个条件编译;

#idef 一般后面接一定宏定义,当这个宏定义有被定义时(可能是头文件中显式地使用#define定义,也有可能是通过编译选项传递进去,比如gcc就支持-Dxxx来定义宏定义),后面的代码将会参与编译,否则就不参与;

#if defined(xxx) 这是#ifdef的另一种写法,本质是等价的;

#endif 表示条件编译代码的结束。

这里有几点补充一下:

一个宏定义xxx没有被定义时,它的值默认是0,所以使用**#if xxx**的结果是false;

#ifdef和**#if defined(xxx)这种写法只管宏定义,是定义了还是没有定义,而不管宏定义的值是多少;如果既要关注有没有定义,又要关注定义的值是多少,请使用#if**;

使用**#if时,后面的表达式是可以使用 &&、||**等逻辑运算符的。

在这种条件编译的预处理下,往往我们看到很多可裁剪、可移植的代码都是使用这些手段来实现。

举个rt-thread内核组件代码的例子:

/* 摘自rt-thread/components/finsh/finsh.h片段 */ /* * Copyright (c) 2006-2021, RT-Thread Development Team * * SPDX-License-Identifier: Apache-2.0 * * Change Logs: * Date Author Notes * 2010-03-22 Bernard first version */ #ifndef __FINSH_H__ #define __FINSH_H__ #include #if defined(_MSC_VER) #pragma section("FSymTab$f",read) #endif typedef long (*syscall_func)(void); #ifdef FINSH_USING_SYMTAB #ifdef __TI_COMPILER_VERSION__ #define __TI_FINSH_EXPORT_FUNCTION(f) PRAGMA(DATA_SECTION(f,"FSymTab")) #endif #ifdef FINSH_USING_DESCRIPTION #ifdef _MSC_VER #define MSH_FUNCTION_EXPORT_CMD(name, cmd, desc) \ const char __fsym_##cmd##_name[] = #cmd; \ const char __fsym_##cmd##_desc[] = #desc; \ __declspec(allocate("FSymTab$f")) \ const struct finsh_syscall __fsym_##cmd = \ { \ __fsym_##cmd##_name, \ __fsym_##cmd##_desc, \ (syscall_func)&name \ }; #pragma comment(linker, "/merge:FSymTab=mytext") #elif defined(__TI_COMPILER_VERSION__) #define MSH_FUNCTION_EXPORT_CMD(name, cmd, desc) \ __TI_FINSH_EXPORT_FUNCTION(__fsym_##cmd); \ const char __fsym_##cmd##_name[] = #cmd; \ const char __fsym_##cmd##_desc[] = #desc; \ const struct finsh_syscall __fsym_##cmd = \ { \ __fsym_##cmd##_name, \ __fsym_##cmd##_desc, \ (syscall_func)&name \ }; #else #define MSH_FUNCTION_EXPORT_CMD(name, cmd, desc) \ const char __fsym_##cmd##_name[] RT_SECTION(".rodata.name") = #cmd; \ const char __fsym_##cmd##_desc[] RT_SECTION(".rodata.name") = #desc; \ RT_USED const struct finsh_syscall __fsym_##cmd RT_SECTION("FSymTab")= \ { \ __fsym_##cmd##_name, \ __fsym_##cmd##_desc, \ (syscall_func)&name \ }; #endif #else #ifdef _MSC_VER #define MSH_FUNCTION_EXPORT_CMD(name, cmd, desc) \ const char __fsym_##cmd##_name[] = #cmd; \ __declspec(allocate("FSymTab$f")) \ const struct finsh_syscall __fsym_##cmd = \ { \ __fsym_##cmd##_name, \ (syscall_func)&name \ }; #pragma comment(linker, "/merge:FSymTab=mytext") #elif defined(__TI_COMPILER_VERSION__) #define MSH_FUNCTION_EXPORT_CMD(name, cmd, desc) \ __TI_FINSH_EXPORT_FUNCTION(__fsym_##cmd); \ const char __fsym_##cmd##_name[] = #cmd; \ const struct finsh_syscall __fsym_##cmd = \ { \ __fsym_##cmd##_name, \ (syscall_func)&name \ }; #else #define MSH_FUNCTION_EXPORT_CMD(name, cmd, desc) \ const char __fsym_##cmd##_name[] = #cmd; \ RT_USED const struct finsh_syscall __fsym_##cmd RT_SECTION("FSymTab")= \ { \ __fsym_##cmd##_name, \ (syscall_func)&name \ }; #endif #endif /* end of FINSH_USING_DESCRIPTION */ #endif /* end of FINSH_USING_SYMTAB */

如果是第一次接触这种条件编译的代码,或者即便是老手,第一次阅读这种N层条件编译嵌套的代码时,分分钟被劝退。

所以在这一小节讲到的条件编译问题,不单单是代码编译的时候会遇到,你首先阅读代码就会遇到。

如果不幸你的代码显示在被条件编译括起来的地方编译报错了,那么你第一时间应该要搞清楚你的代码究竟哪个代码块是应该被编译的,因为很多时候往往是因为你的宏定义开关没有正确配置,而导致把不该编译的代码编译进去,自然就编译报错了。

所以面对这种条件编译的代码,第一时间就是弄明白,并确认被编译器预处理展开后的代码,是不是你要的代码?

怎么确认呢?

你可以尝试跳到 4.2.2 章节提前了解。

因篇幅问题,这里仅截取一个论坛中关于条件编译的问题,以供大家参考。

3.3 编译阶段

编译阶段,对于新手而言,可能也是一个编译出错的重灾区。

为什么呢?

因为这里就涉及到C语言的基础语法了,对基础语法的不够了解和理解不够细致,就可能会出一些编译错误。

下面举几个比较典型的例子看看:

例子1:这个编译报错的原因是结构体原型升级了,名称也改了,然后新的版本中某些成员变量没有了,进而引发语法报错。

例子2:这个问题的核心是没有包含对应的头文件没导致结构体类型无法识别,进而报了语法错误。

例子3:这个问题本质是因宏开关不恰当导致结构体的成员变量被消失而引发的语法错误问题。

3.4 汇编阶段

汇编阶段,相对来说,编译出错的情况会少很多,为啥呢?

这是因为,我们大部分时候都是写高级的C语言,很少部分是写汇编代码,如果由C代码到汇编代码这个阶段出错,很有可能就是传递给编译的参数没搞对。

很典型的就是ARM处理器下,它的ARM指令集和Thumb指令集是不一样的;如果你写的是C语言和汇编混合编程(很多底层代码都有这种写法需求),那么你一定要搞清楚,你写的是ARM指令还是Thumb指令,对应传递给编译器的编译选项是不一样的。

下面举几个比较典型的例子看看:

例子1:这个应该是汇编指令的问题,问题帖子中有给答复。

例子2:不同编译器直接的编译转换问题,涉及到汇编指令,可以到评论席看看大家的答复。

例子3:具体编译器对汇编代码的支持问题,看评论席。

3.5 链接阶段

链接阶段是整个完成编译的关键一步,你写的所有C代码,能不能完整串起来,变成一个可执行程序,就得看这一步,所以往往这一步也是编译报错的重灾区。

这里罗列几种常见的编译链接错误:

3.5.1 undefined reference to ‘xxx’

这个错误真的真的太常见了,很多人一见这个就说是“编译报错”,其实准确的术语应该是“链接报错”。

它的错误的核心就是:xxx函数(符号)在你的所有C代码(已经被编译成.o文件)和你加入参与链接的所有库文件(包括静态库和动态库,当然也包括标准C库),都找不到xxx的实现。

明白了这一点之后,你就应该知道如何解决这种链接报错的编译问题了。

举几个论坛中的问题看看,真的就是重灾区。

解决这类问题,我的思路一般就以下几步:

确认这个xxx符号是函数还是宏定义,还是变量;

如果是宏定义,应该就是你的宏定义没被包含进来,导致宏定义没有被展开;

如果是变量或者函数,确认下这其中的代码是否参与了编译,具体可以看.i文件或者.o文件;

还有一种情况,如果是库函数(标准库或者第三方库),确认下这个函数在哪个库,这个库文件是否在你的链接列表里面,比如gcc编译就是使用-lxxx指定链接的库。

3.5.2 cannot find -lxxx

这种就表示xxx是一个库文件,在链接的时候找不到它,一般解决这类问题,可以从以下几个方面考虑:

首先确认xxx库文件有没有搞错?在gcc编译下,一个libxxx.a的静态库或lixxxx.so的动态库,这里的库名称是xxx而不是libxxx.a或libxxx.so,新手往往容易忽略;

确认下库检索的路劲对不对,比如一些第三方库,在gcc下要使用-Lxxx把你库的路径传进去才能找到你的库;

检查下有没有库函数递归依赖的问题,以及库函数链接顺序的问题,往往也容易出错。

3.5.3 multiple definition of ’xxx‘

这个错误就是如其错误提示的那样,重复定义了,排查问题的关键是找到那里重复定义了。

举几个典型例子看看:

例子1:函数重复定义错误,评论席有答案。

例子2:两个.c文件都有clear函数,链接得时候都搞一起了,能不重复定义吗?static函数了解下。

例子3:main函数重复定义,这个还是比较少见,见评论席。

3.6 转换阶段

这个转换阶段一般出问题的情况也很少,在使用GCC编译的时候,这一步使用的objcopy命令,它的一般用法是:

生成bin文件:

objcopy -O binary xxx.elf xxx.bin

生存hex文件:

objcopy -O ihex xxx.elf xxx.hex

往往在实际工程项目中,还有添加**-R**选项:

objcopy -O binary -R .eh_frame -R .init -R .fini -R .comment xxx.elf xxx.bin objcopy -O ihex -R .eh_frame -R .init -R .fini -R .comment xxx.elf xxx.hex

其中**-R**选项表示:去掉这些段。

$ objcopy -h Usage: objcopy [option(s)] in-file [out-file] Copies a binary file, possibly transforming it in the process The options are: -R --remove-section Remove section from the output --remove-relocations Remove relocations from section

3.7 其他阶段

下载运行阶段 -> 功能测试阶段 -> 解BUG阶段 -> 版本发布阶段

本文对这些阶段不做过多阐述,毕竟每个芯片平台有不同的下载方式及调试运行的方法,每个功能的测试方法也差异很大,每个人调试解决BUG的方法各有不同,各自为政,为我所用即可。

到了版本发布这一阶段,基本就功能稳定了,且已达到规划的功能需求,开始走软件发布流程了,这也是每一个项目最期待能走到的阶段。这里特别需要注意的是(血泪的教训):软件发布一定要有规范的流程,且发布的代码一定要能被追踪到指定的提交记录,否则出了问题,你可能会欲哭无泪,还可能被各种DISS。

4 分享几个经验

4.1 分享几个非常奇葩的编译问题

4.1.1 宏定义的这种写法

例子1:一个freeRTOS的宏定义的问题,之前有写过一篇文章(【freeRTOS开发笔记】记一次坑爹的freerTOS-v9.0.0升级到freeRTOS-v10.4.4),感兴趣的可以一看。

4.1.2 static和inline搞什么

例子2:static与inline修饰函数定义的问题,可以参考这个(帖子](https://club.rt-thread.org/ask/question/431613.html),之前也针对这个问题写过一篇文章(【gcc编译优化系列】static与inline的区别与联系),感兴趣可以一看。

4.1.3 环境变量的锅

例子3:环境变量引发的不可思议的问题?看看这个问题!环境变量的使用,请谨慎!

4.1.4 身边的例子

例子4:最后报一个我实际工作中,同事遇到的朋友,当时排查了一会才发现端倪。

报的错误是:multiple definition of ’xxx‘!

我当时的排查思路也是按照上面罗列的几点一个个排查,发现都不是那些。

奇怪呢!还能玩出花来?最后还是 4.2.2章节的方法,把链接的完整log输出来一看,傻眼了!

居然有重复的**.o文件添加到链接中,这不重复定义**才怪呢!

下面简单复盘下当时的场景,有则改之,无则加勉!

我们整个编译构建是基于Makefile来的,整体划分了N个模块,每个子模块有自己独立的Makefile,

该模块下所有需要参与编译的代码会在Makefile中列出来,

正常的情况下,类似这样的写法:

SRC_C-y := ./src/a.c SRC_C-y += ./src/b.c SRC_C-y += ./src/c.c SRC_C-y += ./src/d.c

这样最后参与链接的.o文件就是:a.o b.o c.o d.o,编译没有问题;

后面这哥们不知道怎么手误碰还是怎么着,把冒号给改成了加号:

SRC_C-y += ./src/a.c SRC_C-y += ./src/b.c SRC_C-y += ./src/c.c SRC_C-y += ./src/d.c

结果最后参与链接的.o文件就是:a.o b.o c.o d.o … a.o b.o c.o d.o …

也就是这个模块的.o文件都拷贝了一份,那当然会报重复定义啊!

后面进一步跟进,发现构建流程有点缺陷,每个子模块的Makefile会被加载两次,

这就导致了如果按第二种写法,SRC_C-y就会变成:./src/a.c ./src/b.c ./src/c.c ./src/d.c ./src/a.c ./src/b.c ./src/c.c ./src/d.c

但是按第一种写法就不会,这个就需要了解下Makefile中 := 和 += 的语法区别了,之前写过一篇文章,感兴趣的可以了解下。

总之,当时觉得这个问题真的有点狗血,大家引以为戒。

4.2 分享几个常用于排查编译问题的方法

4.2.1 打开编译过程的完整log输出

为什么要这么做?

因为这样你能看到你的编译构建环境传递给编译器的具体细节,从而了解更多编译器的行为。

一般做编译构建的时候,为了保持编译log输出的整洁性,都会把编译的完整输出默认关闭,你能看到的就是类似这样的:

CC components/mqtt/src/impl/MQTTConnectClient.o CC components/mqtt/src/impl/MQTTDeserializePublish.o CC components/mqtt/src/impl/MQTTPacket.o CC components/mqtt/src/impl/MQTTSerializePublish.o CC components/mqtt/src/impl/MQTTSubscribeClient.o CC components/mqtt/src/impl/MQTTUnsubscribeClient.o CC components/mqtt/src/impl/iotx_mqtt_client.o CC components/mqtt/src/mqtt_api.o

甚至连CC是哪种编译器你都看不到,可能是gcc,也可能是ARMCC,也可能是其他。

那么如何打开编译log的完整输出呢?你可以尝试下如下命令,在输入make的时候,添加一下控制变量:

make V=1 或 make VERBOSE=1

这样,你看到的编译输出就是非常完整的,具体到你用了哪个编译器,传入了哪些参数,一看便知,只不过log是真的多而长,考验你的对log信息的检索能力的时候来了。

"arm-none-eabi-gcc" -c -MD -DMCU_FAMILY=\"mcu_xxx\" -DSYSINFO_PRODUCT_MODEL=\"XXX_xxx\" -DSYSINFO_DEVICE_NAME=\"xxx\" -DuECC_PLATFORM=uECC_arch_other -mcpu=arm968e-s -march=armv5te -mthumb -mthumb-interwork -mlittle-endian -w -save-temps=obj -DCFG_OS_FREERTOS=1 -DWIFI_BLE_COEXIST -DBK_DEBUG_UART=BK_UART_1 -DBK_CLI_UART=BK_UART_1 -DBK_CLI_ENABLE=0 -DUSR_CLI_MAX_COMMANDS=96 -DBLE_5_0 -DEN_LONG_MTU -DEN_COMBO_NET -DEN_AUTH -ggdb -Os -Wall -Wfatal-errors -fsigned-char -ffunction-sections -fdata-sections -fno-common -std=gnu11 -DPLATFORM=\"xxx\" -include /xxx/application/bbb/xxxos_config.h -Wall -Werror -Wno-unused-variable -Wno-unused-parameter -Wno-implicit-function-declaration -Wno-type-limits -Wno-sign-compare -Wno-pointer-sign -Wno-uninitialized -Wno-return-type -Wno-unused-function -Wno-unused-but-set-variable -Wno-unused-value -Wno-strict-aliasing -Wall -Werror -Wno-unused-variable -Wno-unused-parameter -Wno-implicit-function-declaration -Wno-type-limits -Wno-sign-compare -Wno-pointer-sign -Wno-uninitialized -Wno-return-type -Wno-unused-function -Wno-unused-but-set-variable -Wno-unused-value -Wno-strict-aliasing -Wall -Werror -Wno-unused-variable -Wno-unused-parameter -Wno-implicit-function-declaration -Wno-type-limits -Wno-sign-compare -Wno-pointer-sign -Wno-uninitialized -Wno-return-type -Wno-unused-function -Wno-unused-but-set-variable -Wno-unused-value -Wno-strict-aliasing -I/xxx/platform/mcu/xxx/bk_sdk/config -I/xxx/platform/mcu/xxx/bk_sdk/release -I/xxx/platform/mcu/xxx/bk_sdk/xxx/func/ble_wifi_exchange -I/xxx//components/wireless/bluetooth/ble/host/profile -I/xxx//include/wireless/bluetooth/blemesh -I/xxx//include/network/coap -I/xxx//include/network/hal -I/xxx//include/network/http -I/xxx//include/network/lwm2m -I/xxx//include/network/umesh -I/xxx//include/network/athost -I/xxx//include/network/sal -I/xxx//include/network/netmgr -I/xxx//include/network/rtp -I/xxx//include/utility/yloop -DBUILD_BIN -DCLI_CONFIG_SUPPORT_BOARD_CMD=1 -DCONFIG_xxxos_CLI_BOARD -DCONFIG_xxxos_UOTA_BREAKPOINT -DCONFIG_xxxos_CLI_STACK_SIZE=4096 -DDISABLE_SECURE_STORAGE=1 -DCFG_I2C1_ENABLE=1 -Dxxxos_LOOP -DINFRA_COMPAT -DINFRA_MD5 -DINFRA_NET -DINFRA_SHA256 -DINFRA_TIMER -DINFRA_STRING -Dxxxos_COMP_CLI -Dxxxos_COMP_KV -DMBEDTLS_CONFIG_FILE=\"mbedtls_config.h\" -DCONFIG_HTTP_SECURE=1 -DCOAP_SERV_MULTITHREAD -Dxxxos_COMP_VFS -D__FILENAME__='"mem.c"' -o /xxx/out/bbb/bbb@xxx/modules/platform/mcu/xxx/bk_sdk/xxx/func/lwip_intf/lwip-2.0.2/src/core/mem.o /xxx/platform/mcu/xxx/bk_sdk/xxx/func/lwip_intf/lwip-2.0.2/src/core/mem.c

这个输出log是gcc的,所以得对gcc的编译参数有所了解才行,比如-Dxxx表示宏定义,-Ixxx表示头文件搜索目录等等。

注意:make加V=1或VERBOSE=1,是一般写得比较好的Makefile都会这么做,但不代表每个写Makefile的人都会这么做。

方法一:

与Makefile类似,CMake也有完整输出的开关,在输入make之后,增加一个控制变量:

make VERBOSE=1

不加VERBOSE=1的效果是:

[ 16%] Building C object src/CMakeFiles/yyy.dir/xxx/bn_mp_import.c.o [ 16%] Building C object src/CMakeFiles/yyy.dir/xxx/bn_mp_init.c.o [ 17%] Building C object src/CMakeFiles/yyy.dir/xxx/bn_mp_init_copy.c.o [ 17%] Building C object src/CMakeFiles/yyy.dir/xxx/bn_mp_init_multi.c.o [ 17%] Building C object src/CMakeFiles/yyy.dir/xxx/bn_mp_init_set.c.o [ 17%] Building C object src/CMakeFiles/yyy.dir/xxx/bn_mp_init_set_int.c.o [ 18%] Building C object src/CMakeFiles/yyy.dir/xxx/bn_mp_init_size.c.o [ 18%] Building C object src/CMakeFiles/yyy.dir/xxx/bn_mp_invmod.c.o [ 18%] Building C object src/CMakeFiles/yyy.dir/xxx/bn_mp_invmod_slow.c.o [ 18%] Building C object src/CMakeFiles/yyy.dir/xxx/bn_mp_is_square.c.o [ 19%] Building C object src/CMakeFiles/yyy.dir/xxx/bn_mp_jacobi.c.o [ 19%] Building C object src/CMakeFiles/yyy.dir/xxx/bn_mp_karatsuba_mul.c.o [ 19%] Building C object src/CMakeFiles/yyy.dir/xxx/bn_mp_karatsuba_sqr.c.o [ 20%] Building C object src/CMakeFiles/yyy.dir/xxx/bn_mp_kronecker.c.o [ 20%] Building C object src/CMakeFiles/yyy.dir/xxx/bn_mp_lcm.c.o [ 20%] Building C object src/CMakeFiles/yyy.dir/xxx/bn_mp_lshd.c.o [ 20%] Building C object src/CMakeFiles/yyy.dir/xxx/bn_mp_mod.c.o [ 21%] Building C object src/CMakeFiles/yyy.dir/xxx/bn_mp_mod_2d.c.o [ 21%] Building C object src/CMakeFiles/yyy.dir/xxx/bn_mp_mod_d.c.o

加上VERBOSE=1的效果:

[ 3%] Building C object src/CMakeFiles/yyy.dir/yyy/bncore.c.o cd /yyy/build/x86_release/src && /usr/bin/gcc -DENCRYPTO_MODE=AES -DHAL_SHIELD=0 -I/sysroot/usr/include -I/yyy/inc -I/yyy/inc/hal -I/yyy/src/hal/9x07/linux -I/yyy/inc/hal/log -I/yyy/inc/hal/crypto -I/yyy/inc/hal/crypto/tommath -I/yyy/inc/hal/crypto/skb -I/yyy/inc/hal/asn1 -I/yyy/inc/uicc_framework -I/yyy/inc/uicc_framework/apdu -I/yyy/inc/uicc_framework/channel -I/yyy/inc/uicc_framework/comm -I/yyy/src/uicc_framework/comm -I/yyy/inc/uicc_framework/dispatcher -I/yyy/inc/uicc_framework/profile -I/yyy/inc/uicc_framework/filesystem -I/yyy/inc/uicc_framework/nvm -I/yyy/inc/uicc_framework/utils -I/yyy/inc/uicc_framework/ppi -I/yyy/inc/uicc_framework/tf -I/yyy/inc/applet -I/yyy/inc/applet/common -I/yyy/inc/applet/common/util -I/yyy/inc/applet/common/uicc -I/yyy/inc/applet/ecasd -I/yyy/inc/applet/isdp -I/yyy/inc/applet/usim -I/yyy/inc/applet/isdr -I/yyy/inc/applet/nusim -Wall -pthread -fPIC -Wno-missing-braces -s -m32 -Werror -Wno-unused-function -Wno-unused-variable -Wno-unused-value -O2 -Os -Wmissing-prototypes -Wstrict-prototypes -DDEBUG=0 -I/yyy/build/x86_release -o CMakeFiles/yyy.dir/yyy/bncore.c.o -c /yyy/src/yyy/bncore.c

其实CMake最后的本质就是Makefile。

方法二:

在CMakeLists.txt配置文件的相应位置中,新增以下设置项,也可以达到上面的效果。

set ( CMAKE_VERBOSE_MAKEFILE on )

注意:有了这个配置之后呢,就只需要输入make了,但是它的不好之处就是,你不想完整输出的时候,你还得去改CMakeLists.txt配置文件,使用上没有那么方便。所以我个人推荐使用方法一。

这个构建方式也是RTT支持的,我们可以看下它的编译完整输出是怎么样的:

scons --verbose

不加控制项,输出如下:

$ scons scons: Reading SConscript files ... scons: done reading SConscript files. scons: Building targets ... scons: building associated VariantDir targets: build CC build/kernel/components/dfs/src/dfs.o CC build/kernel/components/dfs/src/dfs_file.o CC build/kernel/components/dfs/src/dfs_fs.o CC build/kernel/components/dfs/src/dfs_posix.o CC build/kernel/components/drivers/i2c/i2c-bit-ops.o CC build/kernel/components/drivers/i2c/i2c_core.o CC build/kernel/components/drivers/i2c/i2c_dev.o CC build/kernel/components/drivers/misc/pin.o CC build/kernel/components/drivers/mtd/mtd_nand.o CC build/kernel/components/drivers/mtd/mtd_nor.o CC build/kernel/components/drivers/rtc/rtc.o CC build/kernel/components/drivers/rtc/soft_rtc.o CC build/kernel/components/drivers/sdio/block_dev.o CC build/kernel/components/drivers/sdio/mmc.o CC build/kernel/components/drivers/sdio/mmcsd_core.o CC build/kernel/components/drivers/sdio/sd.o CC build/kernel/components/drivers/sdio/sdio.o CC build/kernel/components/drivers/serial/serial.o CC build/kernel/components/drivers/spi/sfud/src/sfud.o CC build/kernel/components/drivers/spi/sfud/src/sfud_sfdp.o CC build/kernel/components/drivers/spi/spi_core.o

添加verbose控制项之后的输出:

arm-none-eabi-gcc -o build/kernel/components/finsh/msh_file.o -c -march=armv7-a -marm -msoft-float -Wall -g -gdwarf-2 -O0 -DHAVE_CCONFIG_H -D__RTTHREAD__ -DRT_USING_NEWLIB -I. -Idrivers -Iapplications -I/yyyrt-thread/include -I/yyyrt-thread/libcpu/arm/common -I/yyyrt-thread/libcpu/arm/cortex-a -I/yyyrt-thread/components/cplusplus -I/yyyrt-thread/components/drivers/include -I/yyyrt-thread/components/drivers/spi -I/yyyrt-thread/components/drivers/spi/sfud/inc -I/yyyrt-thread/components/net/sal_socket/include -I/yyyrt-thread/components/net/sal_socket/include/socket -I/yyyrt-thread/components/net/sal_socket/impl -I/yyyrt-thread/components/net/sal_socket/include/dfs_net -I/yyyrt-thread/components/net/sal_socket/include/socket/sys_socket -I/yyyrt-thread/components/net/netdev/include -I/yyyrt-thread/components/net/lwip-2.1.2/src -I/yyyrt-thread/components/net/lwip-2.1.2/src/include -I/yyyrt-thread/components/net/lwip-2.1.2/src/arch/include -I/yyyrt-thread/components/net/lwip-2.1.2/src/include/netif -I/yyyrt-thread/components/libc/compilers/common -I/yyyrt-thread/components/libc/compilers/gcc/newlib -I/yyyrt-thread/components/libc/posix/src -I/yyyrt-thread/components/libc/posix/pthreads -I/yyyrt-thread/components/libc/posix/signal -I/yyyrt-thread/components/libc/posix/termios -I/yyyrt-thread/components/libc/posix/aio -I/yyyrt-thread/components/libc/posix/getline -I/yyyrt-thread/components/lwp -I/yyyrt-thread/components/dfs/include -I/yyyrt-thread/components/dfs/filesystems/devfs -I/yyyrt-thread/components/dfs/filesystems/elmfat -I/yyyrt-thread/components/dfs/filesystems/ramfs -I/yyyrt-thread/components/dfs/filesystems/romfs -I/yyyrt-thread/components/finsh -I/yyyrt-thread/examples/utest/testcases/kernel /yyyrt-thread/components/finsh/msh_file.c

至于具体的编译输出的log啥含义,还得看具体的编译器。gcc是我们常用的,这个还是要熟悉。

4.2.2 打开编译过程的中间文件的输出

这个选项我在上一篇文章也提到过,这里用一个小结再简单介绍下,对于排查编译问题以及排查汇编代码级的性能问题,用过都说好。

这个参数就是-save-temps=obj,我们来实践下:

gcc/gcc_helloworld$ ./build.sh clean Clean build done ! gcc/gcc_helloworld$ gcc/gcc_helloworld$ ls build.sh main.c README.md sub.c sub.h gcc/gcc_helloworld$ gcc/gcc_helloworld$ ./build.sh allinone gcc -c main.c -o main.o -save-temps=obj gcc -c sub.c -o sub.o -save-temps=obj gcc main.o sub.o -o test gcc/gcc_helloworld$ gcc/gcc_helloworld$ ls build.sh main.c main.i main.o main.s README.md sub.c sub.h sub.i sub.o sub.s test

就这样,.i文件、.s文件、以及**.o文件**都同时输出来了。

如果工程中,只有一个main.c的源文件的话,还可以这样就一步搞定。

gcc main.c -o test -save-temps=obj

这些**.i文件**、.s文件、以及**.o文件**,我们称之为中间临时文件。

总结:

查看预处理之后的代码文件,请看**.i**文件 (再复杂的条件编译你也不怕,在这个文件里面,全部暴露原型)

查看C代码生成的对应汇编代码,请看**.s**文件

查看C代码对应的符号表信息,请看**.o**文件

其实KEIL里面也有对应的设置项,我手上没有现成的IDE环境,我网上找了一篇文章,介绍得还不错,大家可以参考下。

4.3 友情提醒

生命有限,有效编码。

请尊重你自己写的每一行代码。

请保证你的代码编译永远都是:0 warning 0 error 0 bug 。

5 新年祝福

愿大家新的一年,如虎添翼,展翅高飞,2022,逐梦起航!

6 更多分享

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

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

【CSDN主页:架构师李肯】

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

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

【GCC专栏】

【信息安全专栏】

【RT-Thread开发笔记】

【freeRTOS开发笔记】

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

Windows 汇编语言

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:C 语言代码风格之 Linux 内核代码风格
下一篇:Spark Core快速入门系列(3) | <Transformation>转换算子
相关文章