标签 开源 下的文章

关于编译阶段符号多重定义的问题

印象中关于编译以及链接的问题早已是老生常谈了。但今天又遇到了一个这样的问题,这里还总想提及一下下^_^。

 
这次要说的问题依旧发生在使用lcut进行单元测试的过程中。一位同事在编译使用了mock函数的测试用例代码时出现了"multiple definition of 'xxx'“的错误。这里简单模拟其场景如下:
 
/* testall.c */
 
/* mock lib function */
int lib_function(…) {
    …
    return (int)LCUT_MOCK_RETV();
}
 
int function_to_be_tested(…) {
    …
    ret = lib_function(…);
    …
}
 
void test_case(lcut_tc_t *tc, void *data) {
    ret = function_to_be_tested(…);
    LCUT_INT_EQUAL(tc, 0, ret);
}
 
lib_function是静态共享库中的一个接口,但这里被mock了。不过由于一些其他test_case使用了静态共享库(.a)的其他接口,因此在编译时程序链接了这个静态共享库。但结果编译器却报错:lib_function被多重定义了。
 
经过各种排查(编译器命令行中的目标文件与库链接顺序是否正确等),我们发现编译器报错的原因居然是忘记mock几个与lib_function同属一库模块(xx.o)的接口。
 
这里就不拐弯抹角了。由于漏掉了一些本该mock的接口,因此程序在编译testall.c时有许多unresolved的符号需要到静态共享库中去查找。这里又涉及到了符号resolve的过程,而更为重要的一点是要弄清楚编译器是如何对待静态共享库中那个拥有testall.o中未resolved的符号的库模块的(一个静态库.a文件实际上是由诸多库模块.o文件组合而成的)。我们来看看下面例子。
 
一个libcommon.a的组成如下:
libcommon.a
    – foo.o
        – function: foo1
        – function: foo2
    – bar.o
        – function: bar1
        – function: bar2 
 
我们来看一下一个调用了foo1函数且链接了libcommon.a的可执行文件(a.out,对应的源文件main.c)中都有哪些已定义的符号:
$ nm a.out
080483d4 T foo1
080483b4 T main
080483e2 T foo2
 
通过nm输出的结果可以看到,最终的可执行程序中居然包含了程序并未调用的函数foo2的定义。似乎一切都清晰了:编译器在libcommon.a的foo.o中找到了unresolved的符号foo1,但编译器并非只是将foo1的定义放入最终的可执行文件中,而是将foo.o从libcommon.a中取出,并将其与main.o放在一处同等对待,编译器会扫描foo.o中所有的符号,并确保其中的符号都是具有定义的,这样才能保证最终的可执行程序中所有的符号都是具有定义的。
 
现在我们可以回过头来回答本文开始处所遇到的那个"多重定义"的问题了。因为testall.c中存在未resolved的符号,即那些被漏掉的未mock的库接口,因此编译器找到了静态共享库中定义了这些库接口的库模块(某个.o文件),但编译器并非只是处理这些符号,和上面的例子一样,编译器还会扫描这个库模块文件中的所有符号以确保所有符号都有定义。而就在这个过程中编译器发现了其中有的符号(比如lib_function)的定义与testall.c中mock的同名接口(lib_function)定义相冲突,从而才作出了错误提示。
 
之前写过一篇文章《从mock malloc说起》,其中有关于编译过程中符号resolve的详细说明,有兴趣的朋友不妨看看。

lcut 0.3.0版本发布

lcut单元测试框架在我的项目中应用已经有一段时间了,项目组的同事对lcut的使用也是越来越熟悉,这不今天一位同事还提出了一个新需求,需求大致是这样的。

 
在实际项目中,经常遇到这类情况:
 
int bar(…) {
    int ret;
 
    ret = foo(…);
    /* assert ret */
    …
 
    ret = foo(…);
    /* assert ret */
    …
 
    ret = foo(…);
    /* assert ret */
    …
}
 
上述代码中被测函数接口bar的实现中多次调用了某函数foo。这样当我们用mock方式测试bar这个函数时,可能需要多次重复设置foo的返回值以及输出函数的值,就像这样:
 
void tc_test_bar_return_ok(lcut_tc_t *tc, void *data) {
    LCUT_RETV_RETURN(foo, 0);
    LCUT_RETV_RETURN(foo, 0);
    LCUT_RETV_RETURN(foo, 0);
 
    LCUT_ARG_RETURN(foo, 1);
    LCUT_ARG_RETURN(foo, 1);
    LCUT_ARG_RETURN(foo, 1);
 
    LCUT_INT_EQUAL(tc, 0, bar(…));
    …
}
 
我的同事希望lcut能提供一个接口:支持一次调用,设置多次mock obj的返回值或输出参数,使用这样的接口后,上述代码就可以简化为:
 
void tc_test_bar_return_ok(lcut_tc_t *tc, void *data) {
    LCUT_RETV_RETURN_COUNT(foo, 0, 3);
    LCUT_ARG_RETURN_COUNT(foo, 1, 3);
 
    LCUT_INT_EQUAL(tc, 0, bar(…));
}
 
这个需求提的非常好,看起来更像是一种语法糖(syntactic sugar),用于简化代码编写。于是乎下午我就为lcut增加了两个有用的宏:LCUT_RETV_RETURN_COUNT和LCUT_ARG_RETURN_COUNT。
 
正如上面所说,这两个宏可在一次调用中多次设置某个mock obj的返回值和输出参数值,两个宏的原型如下:
 
#define LCUT_RETV_RETURN_COUNT(fcname, value, count) do { \
        lcut_mock_obj_return(#fcname, (void*)value, __FUNCTION__, __LINE__, __FILE__, MOCK_RETV, count); \
    } while(0);
 
#define LCUT_ARG_RETURN_COUNT(fcname, value, count) do { \
        lcut_mock_obj_return(#fcname, (void*)value, __FUNCTION__, __LINE__, __FILE__, MOCK_ARG, count); \
    } while(0);
 
只是比之前提供的LCUT_RETV_RETURN和LCUT_ARG_RETURN多了一个宏参数count。count用于指出对mocked obj进行多少次返回值或输出参数的设置。
 
另外当count传入-1时,其语义为无论mocked object被使用多少次,其返回值或输出参数值都是一样的,即使用LCUT_RETV_RETURN_COUNT或LCUT_ARG_RETURN_COUNT时设置的那个值,直到下一次调用这两个宏进行重新设置时,值才会发生变化。例如上面的例子我们也可以改写为:
 
void tc_test_bar_return_ok(lcut_tc_t *tc, void *data) {
    LCUT_RETV_RETURN_COUNT(foo, 0, -1);
    LCUT_ARG_RETURN_COUNT(foo, 1, -1);
 
    LCUT_INT_EQUAL(tc, 0, bar(…));
}
 
这样无论后续再调用多少次bar函数,foo的返回值总是0,输出参数也总是1。
 
增加了这两个宏后,lcut的版本号也小升了一位,最新版本是lcut-0.3.0-rc1,其中还增加了一个针对lcut mock功能的example – mock_test.c。同时Google Code上的lcut guide也做了更新,对新增的宏的用法进行了简要说明。
 
就这样,lcut 0.3.0版本算是发布了,后续还会经过内部的细致测试,如果没有什么问题,就会去掉rc。
如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats