标签 Build 下的文章

'符号连接'那些事儿

我们在编译自己开发的程序或者一些开源软件的时候,常常遇到类似如下的编译器错误信息:
未定义 文件中的
符号 在文件中
i /var/tmp//ccU4sj6I.o
func /var/tmp//ccU4sj6I.o

ld: 致命的: 符号参照错误. 没有输出被写入a.out
collect2: ld returned 1 exit status

或"undefined reference to 'i' or undefined reference to 'func'"
或"error LNK2001: unresolved external symbol _func" (Visual C++编译器输出)

通过加入-v编译选项(GCC的编译选项),我们可以清晰的看到错误输出并非出自编译阶段(生成.o或.obj目标文件),而是产生于连接阶段,即将.o文件转换成最的可执行文件阶段。

GCC错误信息中用的是undefined reference,而VC用的则是unsesolved external symbol。感觉用"unresolved external symbol"更容易理解一些。连接阶段的symbol到底所指什么呢?我们看下面这段代码:
/* testsymbollink.c */
extern int myvar;
extern void myfunc(int a, int b);
 int main() {
  myvar = 7;
  myfunc(100, 200);
  return 0;
  }

我们通过gcc -S输出其汇编码:
/* testsymblolink.s */
.file "testsymbollink.c"

.section ".text"
.align 4
.global main
.type main,#function
.proc 04

main:
!#PROLOGUE# 0
save %sp, -112, %sp
!#PROLOGUE# 1
sethi %hi(myvar), %o0
or %o0, %lo(myvar), %o1
mov 7, %o0
st %o0, [%o1]
mov 100, %o0
mov 200, %o1
call myfunc, 0
nop
mov 0, %o0
mov %o0, %i0
nop
ret
restore

.LLfe1:
.size main,.LLfe1-main
.ident "GCC: (GNU) 3.2"

对于上述汇编码,我们一般理解是包含三个部分:
1) 描述型信息:如:.file、.section、.align、.type等,这些信息用于直到连接器正确的连接代码而使用的。
2) 汇编指令:如mov、st等。
3) 一些待resolve的符号:如main、myvar和myfunc。

连接器负责将.o目标代码进行处理并生成可执行文件。在连接器处理时,描述型信息告知连接器.o中的指令和数据的应该存放的位置属性信息;汇编指令则直接转成机器码;只有那些待resolve的符号需要连接器做慎重处理:main是默认的入口函数的符号,连接器默认会认识,其余的符号连接器就要在其输入的.o文件中或者指定连接的库(.a)中寻找符号的定义了,就如上面的main。如果是数据,则需要获取其位置和大小,如果是函数,则要获取其具体的实现了。

我们再举一个例子来对比一下:
int myvar = 0;
void myfunc(int a, int b) {
;
}

int main() {
myvar = 7;
myfunc(100, 200);
}

转换成汇编码为:
.file "testsymbollink1.c"
.global myvar
.section ".data"
.align 4
.type myvar,#object
.size myvar,4

myvar:
.long 0
.section ".text"
.align 4
.global myfunc
.type myfunc,#function
.proc 020

myfunc:
!#PROLOGUE# 0
save %sp, -112, %sp
!#PROLOGUE# 1
st %i0, [%fp+68]
st %i1, [%fp+72]
nop
ret
restore

.LLfe1:
.size myfunc,.LLfe1-myfunc
.align 4
.global main
.type main,#function
.proc 04

main:
!#PROLOGUE# 0
save %sp, -112, %sp
!#PROLOGUE# 1
sethi %hi(myvar), %o0
or %o0, %lo(myvar), %o1
mov 7, %o0
st %o0, [%o1]
mov 100, %o0
mov 200, %o1
call myfunc, 0
nop
mov %o0, %i0
nop
ret
restore

.LLfe2:
.size main,.LLfe2-main
.ident "GCC: (GNU) 3.2"
从上述汇编码我们可以看到,myvar和myfunc都给出定义,这样连接器工作的时候就不会因找不到这两个符号而报错了。符号的定义既可以在同一个.o中,也可以在不同的.o中,这样便于软件分层次、分模块开发。

对比上面两个example中myvar和myfunc的书写方式:
extern int myvar;
extern void myfunc(int a, int b);

int myvar = 0;
void myfunc(int a, int b) { … }
可以看出,变量和函数的声明和定义的方式直接会影响到其连接的属性。

那么在C语言中,声明和定义又有哪些事呢?我们下面道来^_^
在"C语言参考手册"的第四章作者给了'声明'一个诠释:"声明一个名称就是把一个标识符与某个C语言对象相关联",这句很是给人以启发。名称、标识符是什么呢?就是一个符号;C语言对象呢?对于数据对象来说,就是一块存储块;对于函数对象来说,就是函数的定义,当然这个定义也是要存储在TEXT SECTION的。真正将标识符和C语言对象相关联的工作是在连接阶段完成的。我们的C源代码需要给连接器足够的信息,以保证其正确无误的将每个标识符(符号)与对应的存储相关联。C语言中的声明恰恰给予连接器以有效帮助。

C语言提供了extern和static存储说明符来对应两种连接属性:外部连接(External linkage)和内部连接(Internal linkage)。在源程序顶层的声明中,内部与外部的连接属性区别在于该符号是否为多个翻译单元(translate unit)的所共享。顶层static修饰的符号只能在其所在翻译单元中寻找C语言对象;而顶层extern修饰的符号既可以在其所在的翻译单元寻找C语言对象,也可以在其他翻译单元中寻找。

//foo.c
extern int i;
static int j;
extern void e_func(int a);
static void s_func(void);

int main() {
e_func(1);
s_func();
i = 17;
j = 16;
}
对于变量i而言,连接程序必须在其他翻译单元中查找其相关联的对象;如果找不到,则报错;
对于变量j而言,连接程序在其所在翻译单元中寻找相关联的对象,与i不同的是,如果找不到,这个声明就会被转化为定义;这个对象的初值被置为0;
对于函数e_func而言,连接程序必须在其他翻译单元中查找其相关联的对象;如果找不到,则报错;
对于函数e_func而言,连接程序必须在其所在翻译单元中查找其相关联的对象;如果找不到,则报错。

我们在一些程序中经常看到在顶层声明的变量,既没有extern修饰,也没有static修饰,又不像变量定义那样给出初值,那么这样的变量是如何被对待的呢?我们看例子:
/* testsymbollink2.c */
int myvar;
int g_var = 13;
static int l_var = 19;

int main() {
myvar = 7;
}

翻译成汇编代码后:
.file "testsymbollink2.c"
.global g_var
.section ".data"
.align 4
.type g_var,#object
.size g_var,4

g_var:
.long 13
.align 4
.type l_var,#object
.size l_var,4

l_var:
.long 19
.section ".text"
.align 4
.global main
.type main,#function
.proc 04

main:
!#PROLOGUE# 0
save %sp, -112, %sp
!#PROLOGUE# 1
sethi %hi(myvar), %i0
or %i0, %lo(myvar), %i1
mov 7, %i0
st %i0, [%i1]
nop
ret
restore

.LLfe1:
.size main,.LLfe1-main
.common myvar,4,4
.ident "GCC: (GNU) 3.2"
可以看出来,myvar与g_var、l_var的不同,myvar并未有具体定义信息,而是用.common这个描述信息进行了描述。在C89中这个叫做:tentative definition,也就是"暂时定义"。对于这样的变量,如果连接时发现其他翻译单元中没有同名定义,则系统会给该变量"转正",分配空间;如果在其他翻译单元中有同名定义,则该符号就会关联到那个定义上去。
//1.c
int i;

int main() {
printf("%d\n", i);
}

//2.c
int i = 198;

则gcc 1.c 2.c后执行a.out的结果是输出198。1.c中的i已经关联到了2.c中的i了。如果只gcc 1.c,则输出为0,系统默认给i分配空间并初始化为0。

使用外部连接的变量声明是有风险的,因为编译器很难在多个翻译单元之间做一致性检查。比如:
//3.c
extern int *a;

int main() {
(*a) = 5;
}

//4.c
char a = 'c';

我们gcc 3.c 4.c进行编译并执行a.out,在sparc solaris上会出现"段错误 ((主存储器)信息转储)"的错误。为什么呢?我们还要回到'符号'上来,从汇编码分析:
.file "3.c"
.section ".text"
.align 4
.global main
.type main,#function
.proc 04

main:
!#PROLOGUE# 0
save %sp, -112, %sp
!#PROLOGUE# 1
sethi %hi(a), %i0
or %i0, %lo(a), %i0
ld [%i0], %i1
mov 6, %i0
st %i0, [%i1]
nop
ret
restore

.LLfe1:
.size main,.LLfe1-main
.ident "GCC: (GNU) 3.2"

.file "4.c"
.global a
.section ".data"
.type a,#object
.size a,1

a:
.byte 99
.ident "GCC: (GNU) 3.2"

再重申:两个翻译单元中的a是通过符号形式联系在一起的。3.c中的符号a关联到了4.c中的a,而4.c中的a是一个char类型的变量,这点3.c并不知情,仍将它当作int*用,尝试将a的内容作为地址,去操作这个地址;由于a中的值是99,显然这不是一个应用层合法的地址,出core也就是必然的了。
同样对于函数也是如此,函数不过是一段指令集合,标识这个指令集合的也是'符号',不同翻译单元间也是靠符号关联在一起的。
//5.c
extern void func();

int main() {
func();
}

//6.c
void func(int a, int b) {
printf("%d\n", a + b);
}

我们通过gcc 5.c 6.c编译后,执行a.out,得到-13236124(不同环境得到的值不一样),这显然乱了套,func的调用者并没有给func传入参数,但是func并不知情,还是一味的通过%ebp在栈上定位两个参数后,将其相加输出,显然这两个值是随机的值,结果也是随机的。编译器显然对于检查func是否被正确调用显得束手无策。编译器唯一能做的就是在同一个翻译单元内部检查函数调用是否符合extern声明,所以要尽量使用原型声明,以保证在同一个翻译单元内函数调用的正确。

//7.c
extern void func(int a, char *p);

int main() {
func(5, 10); //warning: passing arg 2 of `func' makes pointer from integer without a cast
}

成功Build ACE

近期公司实行新的绩效考核机制,我的考核目标中就有一项叫做:"成功使用新技术、框架、思路等至少3个",呵呵,先不论绩效考核机制是否合理,既然已经这样了那就需要去适应。一直在做Network Application,早就知道ACE在业界中的名气,这回有理由找个时间好好挖掘一下ACE的思路,也为我的绩效目标增色啊^_^。

以上只是开个玩笑罢了。上周末去书店看到电子工业出版社再次出版的'C++网络编程卷一',这套书的卷1以及卷2的英文电子版我早已有了,但是还是喜欢抱着纸板书看书的那种感觉,所以就顺便买了下来。翻看了一下,发现里面的内容恰恰是现在我所需要的,如果是前两年看这本书,理解起来肯定不会很透彻,因为那时的心中缺少的是恰恰是问题和困惑,没有了那些东西看书的效果也就大打折扣;反之如果作者的思路恰恰是把你扶上的正确的思维轨道,以帮助你解决了心中的那个结,你的收获就会是大大的。

记得前年的时候曾经下载过ACE并且尝试从源代码Build,结果是失败了,原因现在已经记不清了;这次重新下载ACE-5.5版本在Solaris 9上用G++ 3.2编译,居然顺利通过,其功劳应该归功于ACE的开发者之一Stephen D. Huston的'The ACE Programmers Guide'一书,而且从ACE自带文档中得知,ACE最先就是在Solaris上开发的。

Build过程:
1. 下载ACE的源码包;解包解压,一般你会在当前目录下获得一个名为'ACE_wrappers'的目录;
2. 设置ACE_ROOT环境变量;如我用的是csh,我就会在用户的HOME路径下的.cshrc中增加一个环境变量ACE_ROOT,比如:setenv ACE_ROOT '/export/home1/baim/ACE_wrappers';
3. 切换路径到$ACE_ROOT/ace/下,创建config.h,在这个头文件中,我们需要做一件事,就是include一个你所在的编译平台的头文件,比如我是在Sun Solaris 9上编译的,我的config.h中的内容就是这样的:
//config.h
#include "config-sunos5.9.h"

不同的平台,包含的头文件不同,这些头文件也都在$ACE_ROOT/ace/下,你可以用'ls -l|grep config'来看看究竟有哪些config头文件,选择你所在平台对应的即可。

4. 切换到$ACE_ROOT/include/makeinclude下,创建一个叫'platform_macros.GNU'的文件,同样这里也有平台相关的一堆.GNU文件,我们只需在我们新建的platform_macros.GNU文件中包含对应文件即可。
如我用g++在Solaris 9上编译,我就该选择:platform_sunos5_g++.GNU
//platform_macros.GNU
include $(ACE_ROOT)/include/makeinclude/platform_sunos5_g++.GNU

这个文件里还可以放置make的编译选项,比如我要生成.a文件,我可以这么做:
//platform_macros.GNU
static_libs=1
include $(ACE_ROOT)/include/makeinclude/platform_sunos5_g++.GNU

5. 切换到$ACE_ROOT/ace/下,输入make命令执行即可。编译的过程是漫长的,大约1个小时,之后你就会发现在$ACE_ROOT/ace/下有libACE.so -> libACE.so.5.5.0*、libACE.so.5.5.0*和libACE.a出现了。

构建过程到此结束。

ACE的makefile没有.phony install,所以在你的ACE应用程序里可直接引用$(ACE_ROOT)/ace下面的头文件,直接链接$(ACE_ROOT)/ace下面的libACE.a库即可。

//HelloACE.cpp
#include "ace/Log_Msg.h"

void foo (void);

int ACE_TMAIN (int, ACE_TCHAR *[])
{
  ACE_TRACE(ACE_TEXT ("main"));

  ACE_DEBUG ((LM_INFO, ACE_TEXT ("%IHi Mom\n")));
  foo();
  ACE_DEBUG ((LM_INFO, ACE_TEXT ("%IGoodnight\n")));

  return 0;
}

void foo (void)
{
  ACE_TRACE (ACE_TEXT ("foo"));

  ACE_DEBUG ((LM_INFO, ACE_TEXT ("%IHowdy Pardner\n")));
}

//编译
g++ -o HelloACE HelloACE.cpp -I$ACE_ROOT -I./ -L$ACE_ROOT/ace -lACE  -lsocket -ldl -lgen -lnsl -lposix4 -lthread

这里有几点注意事项:
1、如果libACE.so.5.5.0*和libACE.a都同时在$ACE_ROOT/ace下的话,上面的编译命令默认优先进行动态链接。也就是说编译出来的可执行程序HelloACE在运行的时候如果找不到libACE.so.5.5.0就会报错。
2、如果想进行静态链接的话,可以将$ACE_ROOT/ace下的libACE.so.5.5.0删除或者改名,这样在执行上面的编译命令后即是静态链接。
3、注意g++链接.a和.o时是有顺序的,g++从左到右读入目标文件或.a文件中的符号,如果靠右边的目标文件或者.a文件中没有靠左面的目标文件或者.a文件中的未定义的符号定义的话(或者白话一点说:右边没有左边想要的),程序就会报错。比如我们把上述的编译命令改一下,改为:
g++ -o HelloACE -I$ACE_ROOT -I./ -L$ACE_ROOT/ace -lACE  -lsocket -ldl -lgen -lnsl -lposix4 -lthread HelloACE.cpp
执行命令后,会出现下面错误提示:

未定义                  文件中的
 符号                       在文件中
ACE_Log_Msg::log(ACE_Log_Priority, char const*, …)/var/tmp//ccBl9Q0L.o
ACE_Log_Msg::last_error_adapter()      /var/tmp//ccBl9Q0L.o
ACE_Log_Msg::conditional_set(char const*, int, int, int)/var/tmp//ccBl9Q0L.o
ACE_Log_Msg::instance()             /var/tmp//ccBl9Q0L.o
ld: 致命的: 符号参照错误. 没有输出被写入HelloACE
collect2: ld returned 1 exit status

实际上上面的命令g++ -o HelloACE -I$ACE_ROOT -I./ -L$ACE_ROOT/ace -lACE  -lsocket -ldl -lgen -lnsl -lposix4 -lthread HelloACE.cpp等价于下面两个命令:

g++ -c -I$ACE_ROOT -I./ HelloACE.cpp
g++ -o HelloACE -L$ACE_ROOT/ace -lACE  -lsocket -ldl -lgen -lnsl -lposix4 -lthread HelloACE.o

其实上面错误提示中的"/var/tmp//ccBl9Q0L.o",其实就是一个匿名的HelloACE.o

另外在'The ACE Programmers Guide'一书中,作者给了个Makefile,如下:
BIN   = HelloACE
BUILD = $(VBIN)
SRC = $(addsuffix .cpp,$(BIN))
LIBS =
LDFLAGS = -L$(PROJ_ROOT)/lib
#—————————————————
#Include macros and targets
#—————————————————
include $(ACE_ROOT)/include/makeinclude/wrapper_macros.GNU
include $(ACE_ROOT)/include/makeinclude/macros.GNU
include $(ACE_ROOT)/include/makeinclude/rules.common.GNU
include $(ACE_ROOT)/include/makeinclude/rules.nonested.GNU
include $(ACE_ROOT)/include/makeinclude/rules.bin.GNU
include $(ACE_ROOT)/include/makeinclude/rules.local.GNU

直接用该Makefile会让你的build更简洁,另外还有支持多个.cpp文件的Makefile在那本书里,大家可以参考。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 商务合作请联系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