查看原文
其他

编译器,你在说啥?

格蠹老雷 格友 2023-06-10

05

/

10


Compiler,

What are you

talking about











/////


{ 编译器,你在说啥 }


编译器是软件生产的首要工具,是陪伴程序员每日工作的亲密伙伴。如同每个人都有个性一样,编译器也是有个性的。如果说人的个性主要是通过说话和做事来体现的话,那么编译器的个性主要就是通过它的报错信息体现的。了解这个伙伴的秉性,快速理解它说的话,可以大大提高工作效率。今天就讲一个“如何理解编译器说话”的故事。



   与芯谈

Chat with chips


写好一段代码后,交给编译器编译时的最理想的情况是编译器什么都没有说,没有任何编译错误,完全正确,但是这种情况比较少,或者只有在代码量很小的时候才有可能。五一假期做《与芯谈》直播时,我在直播现场写了一段小程序,目的是希望达到最高的IPC(Instruction Per Cycle),我当着大家的“面”从头写起,写了一个主函数,写了一个子函数,写好后使用gcc的命令行进行编译,一次编译通过,编译器啥也没说,是有点意外的。

没有错误的情况一般只适用于代码量很小的情况。当代码量很大时,特别是代码逻辑比较复杂,又涉及跨平台时,那么编译错误就难以避免了。

这几天,我在把Nano Code的底层模块从Windows移植到Linux。每个模块第一次编译时,都是数千个编译错误,加上难以统计的编译警告。当把数量降到1000以内是个小的里程碑,当把错误数降到100以内就感觉胜利在望了。

Nano Code的底层模块有10来个,大多数已经完成,只剩ntp等少数模块,昨日继续移植ntp时,发现一个古怪的编译错误,引起了我的“格物”兴趣,探究一番后,觉得很值得分享出来。



 古怪的编译错误



写好在编译器输出的众多信息中,我们要讨论的错误信息如下:

1>In file included from ../openocd/ndlink/ndlink.c:39:1>../openocd/src/target/arm_adi_v5.h: In function ‘int dap_dp_poll_register(adiv5_dap*, unsigned int, uint32_t, uint32_t, int)’:1>../openocd/src/target/arm_adi_v5.h:552:71: error: expression cannot be used as a function1>  552 |  LOG_DEBUG("DAP: poll %x, mask 0x%08x, value 0x%08x", reg, mask, value);1>      |                                                                       ^1>../openocd/src/target/arm_adi_v5.h:565:40: error: expression cannot be used as a function1>  565 |   LOG_DEBUG("DAP: poll %x timeout", reg);

文字可能被换行扭曲,为了便于手机上阅读,再放一张图吧:

从上面的说话方式来看,经验丰富的格友应该可以断定,这是GCC报的错。


根据我的经验,GCC与微软的VC相比,两个“人”的说话风格有如下两个大的区别:

01

VC会给每个错误和警告都赋予一个唯一的ID,比如error C2064:xxx,而GCC没有这样的唯一编号。

02

对于头文件中的问题,GCC会比较详细的报告这个头文件是如何被包含进来的,标准的语体是:

In file included from <文件名x>:行号y:

比如上面的:

In file included from ../openocd/ndlink/ndlink.c:39

这个行话的意思是这团错误是关于一个头文件的,它是被文件x的第y行包含进来的。






错误信息的核心陈述




接下来到重点了,那就是错误信息的核心陈述:

error: expression cannot be used as a function

试着翻译一下便是:

错误:表达式不可以被用作函数。

接下来,GCC给出了出错的代码,即下面这行:

10 |  LOG_DEBUG("entering main %d", argc);


交代一下背景,ntp模块的大多数代码都来自著名的开源项目OpenOCD。上面这行代码也是如此。查看相关代码,在helper/log.h可以看到一个用于打印调试信息的宏:

#define LOG_DEBUG(expr, ...) \  do { \    if (debug_level >= LOG_LVL_DEBUG) {\      log_printf_lf(LOG_LVL_DEBUG, \        __FILE__, __LINE__, __func__, \        expr); }\  } while (0)

因为要支持可变参数,所以这个宏的写法有点复杂,使用VC和GCC编译时,写法还不一样。所以我起初以为可能是可变参数的问题,但是很快意识到,不是这么回事。

因为,如果我把宏的名字改为LOG_DEBUG2,调用的地方也改为LOG_DEBUG2,那么上面的编译错误就没有了。

从解决问题的角度讲,我可以做一个全局替换,把LOG_DEBUG都替换为LOG_DEBUG2。但是我不愿意这么做,一是要替换的文件比较多,本地替换了后,下次更新开源代码时还需要再替换,二是想找一下问题的根源。

闭目思考,我想到可能在某个我没发现的地方,还有个LOG_DEBUG定义,但是我使用多个搜索工具,搜遍整个项目的所有文件(包括脚本和数据文件),都没有找到。

后来回想,这时有一个常识影响了我的思考。这个常识便是编译器在编译的预处理阶段时就会做宏展开。LOG_DEBUG是个宏,所以很早就会被展开。






在思考和找寻中,到下班时间了。回家吃过晚饭后,又想起这个问题,打开电脑,我想换个思路,编个小程序,看是否能重现这个问题,如果在小程序中重现,那么就比大项目里好解决很多。

-----





于是我针对本来的问题,写了个小程序,一个log.h,包含LOG_DEBUG宏,另外一个是testlog.cpp,只有一个main函数,在main函数里使用这个宏,核心代码如下:

int main(int argc, char* argv[]){  LOG_DEBUG("entering main %d", argc);  return 0;}

第一次编译时,不能重现问题。

在思考如何重现问题时,我的大脑里灵机一动,想到了问题的根源,根据这个“灵机”,我在main函数上面加了一句:

#define LOG_DEBUG 7

再次编译,问题重现了:

gebox@gebox-VirtualBox:~/nametrap$ gcc testlog.cpp testlog.cpp: In function ‘int main(int, char**)’:testlog.cpp:10:36: error: expression cannot be used as a function   10 |  LOG_DEBUG("entering main %d", argc);      |                                    ^

使用简单代码重现问题后,所有疑惑都水落石出了。

首先,如果把LOG_DEBUG宏展开为7,那么出错的语句就变成:

7("entering main %d", argc);

而7显然不是有效的函数名,所以编译器说“表达式不能被用作函数”,说的很对啊,而且表达的还是比较准确的。




第二个问题是:在本来的项目中,哪里有类似#define LOG_DEBUG 7这样的语句呢?答案是系统的头文件:syslog.h。


搜索syslog.h,观察文件内容,果然有一系列常量定义:

#define  LOG_EMERG    0  /* system is unusable */#define  LOG_ALERT    1  /* action must be taken immediately */#define  LOG_CRIT     2  /* critical conditions */#define  LOG_ERR      3  /* error conditions */#define  LOG_WARNING  4  /* warning conditions */#define  LOG_NOTICE   5  /* normal but significant condition */#define  LOG_INFO     6  /* informational */#define  LOG_DEBUG    7  /* debug-level messages */


这些常量的名字刚好与OpenOCD中log.h的定义重名了。

我把小程序中的#define LOG_DEBUG 7注释掉,加上#include,问题依然可以重现,证明了上述推理。

搜索我的ntp项目,里面果然也包含了syslog.h。

说到这里,问题清楚了,根源是由于log.h中的LOG_DEBUG宏与syslog.h中的LOG_DEBUG宏同名,冲突了。

GCC使用了syslog.h中的定义,把宏展开为数值7,所以报告:

“表达式不可以被用作函数”

搜索OpenOCD的最新代码,虽然在名为jim-syslog.c中包含了syslog.h,但是它在头文件中没有包含,所以没有出现我们讨论的问题。

对于这样的宏重复定义,VC会有下面这样的C4005警告:

1>D:\Work\nano\nd\openocd\ndlink\ntp.h(23,1): warning C4005: “LOG_DEBUG”: 宏重定义1>D:\work\nano\nd\openocd\src\helper\log.h(139): message : 参见“LOG_DEBUG”的前一个定义1>flash_core.c1>D:\Work\nano\nd\openocd\ndlink\ntp.h(23,1): warning C4005: “LOG_DEBUG”: 宏重定义1>D:\work\nano\nd\openocd\src\helper\log.h(139): message : 参见“LOG_DEBUG”的前一个定义

而且,在警告信息中明确指出了前一次的定义位置,这是做的比较好的。

在GCC的文档中可以看到:当宏的定义有含义变化时,gcc也会报警告,但是实际上并没有。即使我加了-Wall之后,也没有看到这个警告。

我用的gcc版本为:

gcc --version

gcc (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0

Copyright (C) 2019 Free Software Foundation, Inc.



顺便说一下,如果在Windows模拟同样的问题,使用VC编译,它报的编译错误也很有趣,特别是翻译为中文之后。

1>D:\Work\nano\nd\openocd\src\flash\nor\flash_core.c(118,11): error C2064: 项不会计算为接受 354 个参数的函数1>D:\Work\nano\nd\openocd\src\flash\nor\flash_core.c(167,11): error C2064: 项不会计算为接受 354 个参数的函数1>D:\Work\nano\nd\openocd\src\flash\nor\flash_core.c(603,13): error C2064: 项不会计算为接受 354 个参数的函数

摘出错误信息,即:

error C2064: 项不会计算为接受 354 个参数的函数

根据错误编号C2064查找文档,可以看到英文原句是:

term does not evaluate to

a function taking N arguments


官方的解释如下:

A call is made to a function through an expression. The expression does not evaluate to a pointer to a function that takes the specified number of arguments.

In this example, the code attempts to call non-functions as functions. The following sample generates C2064:


还有一段很好的示例代码:

// C2064.cppint i, j;char* p;void func() {   j = i();    // C2064, i is not a function   p();        // C2064, p doesn't point to a function}

https://learn.microsoft.com/en-us/cpp/error-messages/compiler-errors-1/compiler-error-c2064?view=msvc-170


对于VC报错中的数字354也是有趣,不知道VC是怎么计算的函数参数,又是如何算出来一个354?根据实际情况,显然是某个代码计算错了,或者说是某个BUG工作了。

今天的软件世界,最基本的问题是代码量巨大。在浩如烟海的代码海洋里,人脑的计算能力常常显得不足。对编译器的设计者而言,编译器的报错信息要有足够的通用性。为了实现这个通用性,那么就必须做抽象,有时抽象了一次可能还不够,必须做多次抽象。

报错信息难读的另一个原因也可能是程序员抽象了之后,又被技术编辑再抽象一次。用道家的话来说,可谓“玄而又玄”。这样反复抽象之后,通用性好了,但是读起来就不像人话了,晦涩难懂。

2023杭州研习班时摄于西湖之畔著名的抱朴道院

这时,我们应该尽可能对它做具象,也就是顺着“抽象”的相反方向思考,通过实际代码和各种小试验,让问题回归到具体的场景中。因为人大多数都是听着各种故事长大的,人脑更适合场景化的感性思考。不过这是我的一家之言,大家姑且听之。

对于GDC或者兰舍的成员,我会把上面的示例代码发到群里,大家可以亲手感受一下。


/////


{ 幽兰 Yourland }

购买幽兰代码本即可成为兰舍会员,与众多技术高手一起成长。

购买可前往淘宝格友小店:

https://m.tb.cn/h.Uuv7fit?tk=N1iIdn8t4CI


 Yourland 








盛格塾是格蠹科技旗下的知识分享平台,是以“格物致知”为教育理念的现代私塾。


本着为先圣继绝学的思想,盛格塾努力将传统文化中的精华与现代科技密切结合,以传统文化和人文情怀阐释现代科技,用现代科技传播传统文化。


访问方式

手机端:微信小程序搜索“盛格塾”

电脑端:下载Nano Code社区版客户端

https://nanocode.cn/#/download

格友公众号

盛格塾小程序

往期 · 精彩推荐

2023杭州研习班回顾

软件工程师就职训练营

腾讯会议为何不闪即退

两只老鼠的罪与罚


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存