当C程式所依赖的汇编函数来自多个目标文件时,可以先将这些目标文件编译成静态库的形式,这样创建可执行文件时,就只需向编译器提供静态库文件名即可,编译器会自动从该库里将依赖的汇编目标文件提取出来...

    本文由zengl.com站长对
    http://pan.baidu.com/share/link?shareid=3717576860&uk=940392313 汇编教程英文版相应章节进行翻译得来。

    另外再附加一个英特尔英文手册的共享链接地址:
    http://pan.baidu.com/share/link?shareid=2345340326&uk=940392313 (在某些例子中会用到)

    本篇翻译对应汇编教程英文原著的第439页到第451页,目前已翻译完前14章 (注意这里的页数不是页脚的页数,而是pdf电子档顶部,分页输入框中的页数,也就是包含了目录,前言部分的总页数,pdf电子档总页数是577,当前已翻译完的页数为451 / 577)。

Creating Static Libraries 创建静态库:

    当C程式所依赖的汇编函数来自多个目标文件时,可以先将这些目标文件编译成静态库的形式,这样创建可执行文件时,就只需向编译器提供静态库文件名即可,编译器会自动从该库里将依赖的汇编目标文件提取出来,并与C程式的目标文件一起生成最终的可执行文件,一般可以将具有相似功能的目标文件组织到一个静态库文件里,例如将数学运算相关的目标文件组织到数学运算库里等。

    从上面静态库的描述里可以看到,静态库中的函数代码会被编译进最终的可执行文件里,稍候还会介绍一种动态库,动态库的函数代码不会编译进最终的可执行文件。

    静态库的编译原理图如下:


图1

    从上图可以看到,function1,function2及function3这三个函数所在的目标文件被编译进一个单独的静态库里,该静态库在与mainprog.c程式进行编译时,编译器会自动从静态库里将C程式所依赖的function2目标文件提取出来,并与C目标文件一起编译进最终的可执行文件里。

    使用静态库的好处在于,由于依赖的函数代码都包含在可执行文件中,所以可执行文件就可以不依赖静态库文件而单独运行起来,如果是使用动态库的话,由于函数的实际代码位于动态库里,在可执行文件中并不存在具体的函数代码,所以可执行文件运行时,必须先加载动态库才能正常运行。

    但是,静态库也有不足的地方:首先,函数代码编译进最终的可执行文件,会增加可执行文件的体积。其次,相同的函数代码可能会存在于多个程式的可执行文件里,例如,mainprog.c程式包含了function2函数代码,另一个otherprog.c程式也可以包含function2的函数代码,那么当mainprog.c程式与otherprog.c程式同时运行时,在内存里就会出现function2函数代码的两个副本,当依赖function2的程式比较多时,内存里就会有多个function2的副本,从而增加了内存开销。如果使用动态库的话,则内存里只会存在一个副本。

    上面介绍了静态库的相关概念,下面就介绍下如何创建静态库。

The ar command 使用ar工具创建静态库:

    在Linux里,可以使用ar工具来创建静态库文件,ar工具可用的命令行选项如下:

Option 
命令行选项
Description 
描述
d Delete files from the archive. 
从库中删除指定的文件
m Move files in the archive. 
调整库里文件的顺序
p Print to stdout a specified file 
in the archive. 
将库里指定文件的内容输出显示到标准输出设备(屏幕)上
q Quickly append a file to the archive. 
将某文件快速添加到库的末尾,不会检查是否存在同名文件
r Insert files (with replacement) 
into the archive. 
将文件添加到库中,如果存在同名文件则执行替换操作
s act as ranlib 
和ranlib命令一样,为库创建索引,以加快编译链接速度
t Display a table of files in the archive. 
将库中包含的文件列举出来
x Extract files from the archive. 
将库中包含的文件解压出来

    上面的命令行选项还可以使用一些修饰符进行修饰,可用的修饰符如下表所示:

Modifier 
修饰符
Description 
描述
a Add new files after existing files in the archive. 
在库里指定的已存在的文件后面添加一个新文件
b Add new files before existing files in the archive. 
在库里指定的已存在的文件前面添加一个新文件
c Create a new archive. 
如果库文件不存在,则创建一个新的库文件
f Truncate names in the archive. 
在向库中添加文件时,对文件名进行截断操作,因为某些系统可能对文件名长度有限制
i Insert new files before existing files in the archive. 
类似于b修饰符,在库里指定的已存在的文件前面添加一个新文件
P Use the full pathname of files in the archive. 
某些GNU ar以外的第三方工具可能会创建出包含完整路径名的库文档,使用该修饰符可以方便ar从其他工具创建的库文档里提取出包含完整路径名的文件出来(GNU ar工具本身是不会创建出包含完整路径名的库文档的,因为这种文档不符合POSIX标准)
s Write an index for the archive. 
将某目标文件的索引写入库文档,或者更新库里已存在文件的索引,你可以将该修饰符与命令行选项一起使用,也可以单独使用该修饰符,使用ar s ...的效果等效于ranlib ...命令
u Update files in the archive 
(replace older ones with newer ones). 
当使用ar r ...命令时,默认会将所有列举出来的文件都添加或替换到库文档里,如果你只想让那些比较新的文件才进行更新的话(比如在外面修改了某个目标文件,则该目标文件的修改时间就会比原来库里的目标文件要新),此时就可以使用该修饰符,不过需要注意的是,不可以使用qu的组合,即不能和q选项一起用,因为q是快速添加文件用的,它不会对修改时间之类的做任何检查
v Use verbose mode. 
使用verbose详细模式,该模式下,可以输出一些额外的信息
V This modifier shows the version number of ar
该修饰符可以显示出ar工具的版本信息

    ar工具的命令格式如下:

ar [-]p[mod [relpos] [count]] archive
       [member...]

    上面p表示命令行选项,mod表示修饰符,relpos可以放置插入位置信息(例如使用a修饰符时,可以将指定的已存在的文件名放置在relpos位置,这样插入文件时,就可以将文件插入到relpos之后),archive表示库文档名称,member...表示需要添加或操作的文件列表,可以使用man ar命令来查看ar工具的详细用法。

Creating a static library file 创建一个静态库文件:

    在使用ar工具创建静态库前,必须先创建好需要添加的目标文件,对于汇编函数,可以使用as -o命令来创建汇编函数对应的.o结尾的目标文件,在创建好所需的目标文件后,就可以使用ar工具来创建静态库文件了,这里需要注意的是,在Linux系统里,静态库文件有一个约定的文件名格式:

libx.a

    上面lib是文件名前缀,x表示具体的库名称,静态库是以.a作为扩展名的,例如:libchap14.a静态库文件名里的lib为前缀,chap14为库名称,然后是.a的扩展名。

    我们下面还是用上一篇文章里的例子来做说明:

# square.s - An example of a function that returns an integer value
.type square, @function
.globl square
square:
	pushl %ebp
	movl %esp, %ebp
	movl 8(%ebp), %eax
	imull %eax, %eax
	movl %ebp, %esp
	popl %ebp
	ret


# areafunc.s - An example of a floating point return value
.section .text
.type areafunc, @function
.globl areafunc
    areafunc:
    pushl %ebp
    movl %esp, %ebp
    fldpi
    filds 8(%ebp)
    fmul %st(0), %st(0)
    fmul %st(1), %st(0)
    movl %ebp, %esp
    popl %ebp
    ret


# cpuidfunc.s - An example of returning a string value
.section .bss
	.comm output, 13
.section .text
.type cpuidfunc, @function
.globl cpuidfunc
cpuidfunc:
	pushl %ebp
	movl %esp, %ebp
	pushl %ebx
	movl $0, %eax
	cpuid
	movl $output, %edi
	movl %ebx, (%edi)
	movl %edx, 4(%edi)
	movl %ecx, 8(%edi)
	movl $output, %eax
	popl %ebx
	movl %ebp, %esp
	popl %ebp
	ret


/* externtest.cpp - An example of using assembly language functions with C++ */
#include <iostream>

using namespace std;

extern "C" {
	int square(int);
	float areafunc(int);
	char *cpuidfunc(void);
}

int main()
{
	int radius = 10;
	int radsquare = square(radius);
	cout << "The radius squared is "<< radsquare << endl;
	float result;
	result = areafunc(radius);
	cout << "The area is " << result << endl;
	cout << "The CPUID is " << cpuidfunc() << endl;
	return 0;
}


    上面有square.s,areafunc.s及cpuidfunc.s三个汇编程式,每个汇编程式里定义了一个汇编函数,这些汇编函数的作用在上一篇文章里已经介绍过,这里就不多说了,最后是一个externtest.cpp的C++程式,上一篇文章中,对这些程式进行编译的具体做法是:

$ as -o square.o square.s
$ as -o areafunc.o areafunc.s
$ as -o cpuidfunc.o cpuidfunc.s
$ g++ -o externtest externtest.cpp square.o areafunc.o cpuidfunc.o
$ ./externtest

The radius squared is 100
The area is 314.159
The CPUID is GenuineIntel

$

    上面输出显示,在进行g++编译时,是将所有依赖的.o文件都列举出来,如果目标文件很多的话,这个文件列表就会比较长了。

    下面看下生成静态库的做法:

$ as -o square.o square.s
$ as -o areafunc.o areafunc.s
$ as -o cpuidfunc.o cpuidfunc.s
$ ar r libchap14.a square.o areafunc.o cpuidfunc.o

ar: creating libchap14.a
$ g++ -o externtest externtest.cpp libchap14.a
$ ./externtest

The radius squared is 100
The area is 314.159
The CPUID is GenuineIntel

$

    上面使用ar r命令来生成libchap14.a的静态库文件,可以看到,g++在生成externtest程式时,只需包含一个libchap14.a的静态库文件即可,另外,使用静态库生成的最终可执行文件在体积大小上,是和直接使用.o目标文件所生成的可执行文件的体积是相等的,因为编译时,会自动从库里提取出所需的目标文件出来,而不会将整个库文件都包含进去。

    下面我们来看下前面提到的ar命令行选项的具体用法,首先看下 t 选项和 d 选项,t 选项可以将库里包含的文件列举出来,而 b 选项的作用则是可以从库文档里删除某个指定的目标文件,使用方法如下:

$ ar t libchap14.a
square.o
areafunc.o
cpuidfunc.o

$ ar d libchap14.a square.o
$ ar t libchap14.a

areafunc.o
cpuidfunc.o

$

    从上面的输出可以看到,一开始使用ar t libchap14.a命令时,显示出libchap14.a库文档里有square.o,areafunc.o及cpuidfunc.o这三个目标文件,当使用ar d libchap14.a square.o命令删除square.o文件后,再次使用ar t libchap14.a命令,就可以看到库文档里只剩下areafunc.o和cpuidfunc.o两个目标文件了。

    再来看下 r 选项和 m 选项,r 选项是用于向库文档里插入文件的,m 选项则是用于调整库文档里文件的顺序的,使用方法如下:

$ ar t libchap14.a
areafunc.o
cpuidfunc.o

$ ar r libchap14.a square.o
$ ar t libchap14.a

areafunc.o
cpuidfunc.o
square.o

$ ar m libchap14.a square.o cpuidfunc.o areafunc.o
$ ar t libchap14.a

square.o
cpuidfunc.o
areafunc.o

$

    一开始libchap14.a里只有areafunc.o和cpuidfunc.o两个目标文件,使用ar r libchap14.a square.o命令后,库文档里就新增了一个square.o文件,默认情况下该文件会添加到最后的位置,接着我们使用ar m libchap14.a square.o cpuidfunc.o areafunc.o命令将库文档里文件的顺序调整为square.o,cpuidfunc.o,areafunc.o的顺序。

    和 r 选项类似的是 q 选项,q 选项用于快速向库文档里添加文件,它不会进行重名检查,即便库里有相同的文件,也会将指定的文件添加到库文档的末尾:

$ ar q libchap14.a square.o
$ ar t libchap14.a

square.o
cpuidfunc.o
areafunc.o
square.o
$

    可以看到,虽然库文档里已经存在了一个square.o目标文件了,但是ar q命令还是将square.o附加到了库文档的末尾,这样库文档里就有两个同名的square.o文件了。

    再来看下 x 选项,该选项用于将库中包含的文件解压出来:

$ ar x libchap14.a square.o
$ ls

libchap14.a  square.o
$ ar x libchap14.a
$ ls

areafunc.o  cpuidfunc.o  libchap14.a  square.o
$

    使用 x 选项时,如果指定了文件名,则将指定的文件给解压出来,如果没有指定文件名,则将库中包含的所有文件都解压出来。

    下面简单看下修饰符的用法:

$ ar t libchap14.a
cpuidfunc.o
areafunc.o
square.o

$ ar ra cpuidfunc.o libchap14.a testfunc.o
$ ar t libchap14.a

cpuidfunc.o
testfunc.o
areafunc.o
square.o

$

    上面输出显示,使用ar ra cpuidfunc.o libchap14.a testfunc.o命令表示在cpuidfunc.o文件的后面插入一个testfunc.o文件,在之前提到的ar命令行格式里有个relpos,cpuidfunc.o就位于relpos的位置,表示需要在该文件的前后插入某个文件,这里使用的是a修饰符,表示在cpuidfunc.o的后面插入文件,如果要在该文件前面进行插入,则可以使用 b 或 i 修饰符。

    另外,还有两个用的比较多的 v 和 s 修饰符:

$ ar tv libchap14.a
rw-r--r-- 0/0    588 May  3 20:08 2014 cpuidfunc.o
rw-r--r-- 0/0    482 May  3 20:57 2014 testfunc.o
rw-r--r-- 0/0    482 May  3 20:08 2014 areafunc.o
rw-r--r-- 0/0    480 May  3 20:08 2014 square.o

$ ar s libchap14.a
$ ranlib libchap14.a
$ nm -s libchap14.a


Archive index:
output in cpuidfunc.o
cpuidfunc in cpuidfunc.o
testfunc in testfunc.o
areafunc in areafunc.o
square in square.o

cpuidfunc.o:
00000000 T cpuidfunc
0000000d C output

testfunc.o:
00000000 T testfunc

areafunc.o:
00000000 T areafunc

square.o:
00000000 T square

$

    上面输出里,t 选项加个 v 修饰符后,就输出了详细的时间和操作权限信息,这里显示的时间是这些目标文件被汇编器创建的时间,而不是它们被添加进库文档的时间。

    另外,上面的ar s ...和ranlib ...命令的作用是一样的,都是生成库文档的索引信息,索引信息可以加快编译器链接库文档的速度,库文档的索引信息可以使用nm -s命令来查看到,从上面nm -s ...命令输出的信息里,可以看到该库包含的函数信息,以及这些函数所在的目标文件信息等。

Using Shared Libraries 使用共享库(或者叫动态库,还可以叫做动态链接库):

    熟悉Windows系统的用户,应该都了解共享库,windows下的共享库是DLL文件,不过DLL文件有个最大的弊端,就是当用户尝试更新DLL文件时,可能会让那些依赖这些DLL文件的程式无法正常运行,很多病毒也喜欢修改DLL文件。

    下面我们来看下Linux系统里共享库的概念,以及如何创建和使用它们。

What are shared libraries 什么是共享库:

    前面讲解静态库时,提到了静态库中的函数代码会被编译进最终的可执行文件,这种方式的缺点如下:
  • If something changes in the function code, every application that uses the function must be
    recompiled with the new version.
    当修改了静态库中某个函数的代码时,每个依赖该库的程式,如果希望获得最新版本的代码,就必须重新进行编译,以便将新版本的静态库给链接进来
  • Multiple programs that use the same functions must all contain the same code. This can make a
    small application program larger than it needs to be, as it must contain all of the code from each
    function used.
    由于需要将静态库的所有依赖的函数代码都编译进可执行文件,所以当依赖的函数代码比较多时,会显著的增加可执行文件的体积
  • Multiple programs running on a system using the same function means that the same function
    is loaded into memory multiple times.
    如果多个程式都包含了静态库里相同的函数,那么当这些程式同时运行时,系统内存里将会存在同一个函数的多个副本,从而会增大内存的开销
    上面的几点缺陷里,后面两点在之前的静态库介绍部分也提到过。共享库可以有效的解决静态库的这些不足,当某个进程需要使用共享库里的函数时,操作系统会自动将共享库中的函数代码加载到内存的公共区域里,进程就可以访问到函数的代码了,如果其他进程也需要访问这些函数的话,可以直接访问已加载到内存里的函数,而无需重新再加载一次共享库,所以相同的函数代码在内存里只有一个副本,从而减少了内存开销,同时,由于共享库包含的函数代码不会编译进最终的可执行文件,所以能有效减少可执行文件的体积。

    下图演示了多个进程调用相同共享库中的函数的方式:


图2

   如果共享库的相关函数被修改了,就只需更新一个共享库文件即可,其他依赖该共享库的程式无需重新编译,就可以使用最新版本的函数,为了防止更新共享库版本时,出现与之前版本的函数的不兼容(如果是windows系统,就可能会因更新共享库而导致依赖该库的程式运行出错),Linux系统提供了一种版本号的机制,有关版本号的实现方式可以参考 http://www.limitedwish.org/?p=85 该链接里的文章(该文章通过详细的例子来解释了和共享库版本控制相关的linkname,soname和realname的概念),下面也会结合我们自己的汇编例子来进行说明。

Creating a shared library 创建一个共享库:

    和静态库类似,共享库也有一个约定的文件名格式:

libx.so

    其中lib为前缀,x表示共享库的名称,.so为扩展名以表示该文件是一个共享库,例如:libchap14.so的文件名(lib为前缀,chap14为共享库的名称,.so为扩展名)。

    要生成共享库的话,可以使用gcc加-shared选项:

$ gcc -shared -o libchap14.so square.o areafunc.o cpuidfunc.o
$ ls libchap14.so

libchap14.so*
$

    上面使用-shared选项生成了libchap14.so的共享库文件,另外,生成该库所需的square.o,areafunc.o及cpuidfunc.o三个目标文件和之前生成静态库的目标文件是一样的。

    如果要链接上面生成的libchap14.so的共享库,则可以使用 -l 选项,使用该选项的话,只需在后面跟随共享库的名称即可,该名称不包含lib前缀和.so的扩展名后缀,在gcc或g++查找时,会自动加入前缀和扩展名后缀,例如-lchap14表示搜索并链接libchap14.so的共享库文件,另外如果要编译的是C程式的话,就使用gcc工具,如果要编译的是C++程式的话,就应该使用g++工具,我们下面要编译的externtest.cpp是C++程式,所以使用的就是g++工具:

$ g++ -o externtest externtest.cpp -lchap14
/usr/lib/gcc/i486-slackware-linux/4.5.2/../../../../i486-slackware-linux/bin/ld: cannot find -lchap14
collect2: ld 返回 1

$

    上面提示cannot find -lchap14,指的是找不到libchap14.so的共享库文件,这是因为gcc或g++工具会根据预定义的目录路径来搜索共享库,可以使用-print-search-dirs选项来查看预定义的搜索库的路径信息:

$ g++ -print-search-dirs
......................................
......................................
库:=/usr/lib/gcc/i486-slackware-linux/4.5.2/:/usr/lib/gcc/i486-slackware-linux/4.5.2/../../../../i486-slackware-linux/lib/i486-slackware-linux/4.5.2/:/usr/lib/gcc/i486-slackware-linux/4.5.2/../../../../i486-slackware-linux/lib/:/usr/lib/gcc/i486-slackware-linux/4.5.2/../../../i486-slackware-linux/4.5.2/:/usr/lib/gcc/i486-slackware-linux/4.5.2/../../../:/lib/i486-slackware-linux/4.5.2/:/lib/:/usr/lib/i486-slackware-linux/4.5.2/:/usr/lib/
$

    g++会从上面一长串的路径里去搜索共享库文件,所以要让g++编译器找到我们的库文件,可以将libchap14.so放置到上面任一目录下,比如放置到/usr/lib目录,再进行编译:

# cp libchap14.so /usr/lib/
# g++ -o externtest externtest.cpp -lchap14
# ls -l externtest

-rwxr-xr-x 1 root root 8057  5月  4 20:15 externtest*
#

    上面的命令提示符由"$"变为了"#",是因为拷贝libchap14.so到/usr/lib目录内需要root权限,如果你没有权限这么做的话,或者不想进行拷贝操作的话,则可以使用-L选项:

$ g++ -o externtest externtest.cpp -L. -lchap14
$ ls -l externtest

-rwxr-xr-x 1 zengl zengl 8057  5月  4 20:15 externtest*
$

    上面-L选项后面跟随了一个"."(小数点,Linux使用点来表示当前工作目录) ,表示将当前目录添加到搜索路径,这样g++在预定义的路径里搜索不到库文件时,就会在当前目录中进行搜索,从而可以正常生成可执行文件。

    虽然生成了externtest可执行文件,但是该文件运行时,会出现如下错误(假设我们使用的是-L的方式来生成可执行文件的,即没有将libchap14.so共享库拷贝到/usr/lib里):

$ ./externtest
./externtest: error while loading shared libraries: libchap14.so: cannot open shared object file: No such file or directory
$ ldd externtest
    linux-gate.so.1 =>  (0xffffe000)
    libchap14.so => not found
    libstdc++.so.6 => /usr/lib/libstdc++.so.6 (0xb7752000)
    libm.so.6 => /lib/libm.so.6 (0xb772c000)
    libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0xb7710000)
    libc.so.6 => /lib/libc.so.6 (0xb75ac000)
    /lib/ld-linux.so.2 (0xb7864000)
$

    上面提示externtest在运行时,找不到libchap14.so文件,我们也可以使用ldd命令来查看externtest所依赖的共享库文件,从ldd输出的信息里也显示 libchap14.so => not found 即找不到libchap14.so共享库,这是因为可执行文件在执行前,会先通过dynamic linker(动态库的加载器)来搜索和加载所需的动态库(即共享库),Linux系统里的动态库的加载器是/lib/ld-linux.so.2,该加载器会从预定义的路径里去搜索,我们可以将LD_DEBUG环境变量的值设置为libs来查看加载器的搜索过程:

$ LD_DEBUG=libs ./externtest
      2326:    find library=libchap14.so [0]; searching
      2326:     search cache=/etc/ld.so.cache
      2326:      trying file=/usr/local/mylib/test/libchap14.so
      2326:     search path=/lib/tls/i686/sse2:/lib/tls/i686:/lib/tls/sse2:/lib/tls:/lib/i686/sse2:/lib/i686:/lib/sse2:/lib:/usr/lib/tls/i686/sse2:/usr/lib/tls/i686:/usr/lib/tls/sse2:/usr/lib/tls:/usr/lib/i686/sse2:/usr/lib/i686:/usr/lib/sse2:/usr/lib        (system search path)
      2326:      trying file=/lib/tls/i686/sse2/libchap14.so
      2326:      trying file=/lib/tls/i686/libchap14.so
      2326:      trying file=/lib/tls/sse2/libchap14.so
      2326:      trying file=/lib/tls/libchap14.so
      2326:      trying file=/lib/i686/sse2/libchap14.so
      2326:      trying file=/lib/i686/libchap14.so
      2326:      trying file=/lib/sse2/libchap14.so
      2326:      trying file=/lib/libchap14.so
      2326:      trying file=/usr/lib/tls/i686/sse2/libchap14.so
      2326:      trying file=/usr/lib/tls/i686/libchap14.so
      2326:      trying file=/usr/lib/tls/sse2/libchap14.so
      2326:      trying file=/usr/lib/tls/libchap14.so
      2326:      trying file=/usr/lib/i686/sse2/libchap14.so
      2326:      trying file=/usr/lib/i686/libchap14.so
      2326:      trying file=/usr/lib/sse2/libchap14.so
      2326:      trying file=/usr/lib/libchap14.so
      2326:    
./externtest: error while loading shared libraries: libchap14.so: cannot open shared object file: No such file or directory
$

    命令行中的 LD_DEBUG=libs ./externtest 命令的意思是在执行./externtest程式之前,先将LD_DEBUG环境变量的值临时设置为libs,然后执行externtest程式时,就可以打印出加载器搜索共享库的过程了,可以看到,加载器会根据/etc/ld.so.cache里的信息(ld.so.cache是由/etc/ld.so.conf配置文件和ldconfig工具生成,下面会进行介绍),从/usr/lib之类的目录里搜索libchap14.so文件,当在这些系统预定义的路径中搜索不到库文件时,就会抛出错误。

    要解决这个问题,第一种方法就是设置LD_LIBRARY_PATH环境变量:

$ LD_LIBRARY_PATH=. ./externtest
The radius squared is 100
The area is 314.159
The CPUID is GenuineIntel

$

    上面将LD_LIBRARY_PATH环境变量设置为"." ,表示将当前工作目录添加到搜索路径里,这样执行externtest时,加载器在系统预定义路径下找不到库文件时,就会到当前目录里查找libchap14.so库文件,并将该共享库加载到内存里,从而让externtest程式能够正常运行,设置环境变量还可以使用export:

$ export LD_LIBRARY_PATH=.
$ ./externtest

The radius squared is 100
The area is 314.159
The CPUID is GenuineIntel

$

    只不过export设置的环境变量对当前shell下所有运行的程式都起作用,而之前的 "LD_LIBRARY_PATH=. ./externtest" 的方式中设置的环境变量仅对externtest程式起作用。

    第二种方式就是修改 /etc/ld.so.conf 的配置文件:

# vim /etc/ld.so.conf
# cat /etc/ld.so.conf

/usr/local/lib
/usr/i486-slackware-linux/lib
/usr/lib/seamonkey
/home/zengl/asm_example/asmfunc
# ./externtest
./externtest: error while loading shared libraries: libchap14.so: cannot open shared object file: No such file or directory
# ldconfig
# ./externtest

The radius squared is 100
The area is 314.159
The CPUID is GenuineIntel

#

    上面通过vim编辑器修改了/etc/ld.so.conf,在该配置文件里加入了/home/zengl/asm_example/asmfunc路径,该路径就是externtest所在的目录,externtest程式所依赖的libchap14.so共享库文件也在该目录内。

    但是,单纯的修改该配置文件并不会有什么作用,externtest执行时还是会报错,这是因为动态库的加载器在搜索路径信息时,实际上依据的是/etc/ld.so.cache缓存文件里的内容,只有通过ldconfig命令才可以将/etc/ld.so.conf配置文件里的内容重新更新到ld.so.cache缓存文件中,因此,在修改了配置文件的内容后,必须执行一次ldconfig命令,动态库的加载器才能找到libchap14.so文件,externtest程式才能正常执行。

    下面我们来看下,Linux系统中,和共享库的版本控制相关的内容。

    如果你不想使用libchap14.so的名字的话,比如,你可能会觉得libchap14.so名字无法保存版本号信息,你希望能够生成libchap14.so.1.0.1这样的含版本号信息的文件名,或者你想使用一个比较独特的库名字,如chap14.myso等,并且你还希望你生成的这些文件名可以被加载器给找到的话,那么就需要先了解Linux系统下共享库的三个概念:linkname,soname和realname 。

    linkname(链接名)主要是给gcc和g++编译器看的,因为gcc与g++编译器的 -l 选项在指定共享库的名称时,是不包含完整的文件名的,例如:-lchap14 表示的其实是libchap14.so文件,所以你无法让编译器去直接链接libchap14.so.1.0.1这类的文件名,或者chap14.myso这类的自定义的文件名,只能是在生成了libchap14.so.1.0.1后,再创建一个libchap14.so的符号链接,并让libchap14.so的符号链接指向libchap14.so.1.0.1的文件,这样编译器在链接libchap14.so时,则实际上链接的就会是libchap14.so.1.0.1文件,所以我们可以将libchap14.so.1.0.1这类自定义的实际生成的库文件名叫做realname(共享库的实际名字),libchap14.so则叫做linkname(链接名) 。

$ gcc -shared -o libchap14.so.1.0.1 square.o areafunc.o cpuidfunc.o
$ ln -s libchap14.so.1.0.1 libchap14.so
$ ls -l
....................................
....................................
lrwxrwxrwx 1 zengl zengl   18  5月  4 21:07 libchap14.so -> libchap14.so.1.0.1*
-rwxr-xr-x 1 zengl zengl 3900  5月  4 21:07 libchap14.so.1.0.1*
....................................
....................................
$ g++ -o externtest externtest.cpp -L. -lchap14
$ LD_LIBRARY_PATH=. ldd externtest
    linux-gate.so.1 =>  (0xffffe000)
    libchap14.so => ./libchap14.so (0xb7816000)
    libstdc++.so.6 => /usr/lib/libstdc++.so.6 (0xb7705000)
    libm.so.6 => /lib/libm.so.6 (0xb76df000)
    libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0xb76c3000)
    libc.so.6 => /lib/libc.so.6 (0xb755f000)
    /lib/ld-linux.so.2 (0xb7819000)
$


    从ldd externtest命令里可以看到,externtest在执行时所加载的是libchap14.so文件,如果你希望直接加载libchap14.so.1.0.1的话,就需要用到共享库的soname,默认情况下生成的共享库是不存在soname的,可以使用gcc的-Wl,-soname选项来进行设置:

$ gcc -shared -Wl,-soname,libchap14.so.1.0.1 -o libchap14.so.1.0.1 square.o areafunc.o cpuidfunc.o
$ readelf -d libchap14.so.1.0.1

Dynamic section at offset 0x4bc contains 22 entries:
  Tag        Type                         Name/Value
 0x00000001 (NEEDED)                     Shared library: [libc.so.6]
 0x0000000e (SONAME)                     Library soname: [libchap14.so.1.0.1]
.........................................
.........................................
$ g++ -o externtest externtest.cpp -L. -lchap14
$ LD_LIBRARY_PATH=. ldd externtest
    linux-gate.so.1 =>  (0xffffe000)
    libchap14.so.1.0.1 => ./libchap14.so.1.0.1 (0xb77dd000)
    libstdc++.so.6 => /usr/lib/libstdc++.so.6 (0xb76cc000)
    libm.so.6 => /lib/libm.so.6 (0xb76a6000)
    libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0xb768a000)
    libc.so.6 => /lib/libc.so.6 (0xb7526000)
    /lib/ld-linux.so.2 (0xb77e0000)
$ readelf -d externtest

Dynamic section at offset 0xb20 contains 24 entries:
  Tag        Type                         Name/Value
 0x00000001 (NEEDED)                     Shared library: [libchap14.so.1.0.1]
 0x00000001 (NEEDED)                     Shared library: [libstdc++.so.6]
 0x00000001 (NEEDED)                     Shared library: [libm.so.6]
 0x00000001 (NEEDED)                     Shared library: [libgcc_s.so.1]
 0x00000001 (NEEDED)                     Shared library: [libc.so.6]
.........................................
.........................................
$ LD_LIBRARY_PATH=. LD_DEBUG=libs ./externtest
      2379:    find library=libchap14.so.1.0.1 [0]; searching
      ..............................
      ..............................
      2379:     search cache=/etc/ld.so.cache
      2379:      trying file=./libchap14.so.1.0.1
      2379:    
      2379:    find library=libstdc++.so.6 [0]; searching
      ..............................
      ..............................
      2379:     search cache=/etc/ld.so.cache
      2379:      trying file=/usr/lib/libstdc++.so.6
      2379:    
      2379:    find library=libc.so.6 [0]; searching
      ..............................
      ..............................
      2379:     search cache=/etc/ld.so.cache
      2379:      trying file=/lib/libc.so.6
      2379:    
      ..............................
      ..............................
The radius squared is 100
The area is 314.159
The CPUID is GenuineIntel
      ..............................
      ..............................
$


    通过-Wl,-soname,libchap14.so.1.0.1选项就可以将libchap14.so.1.0.1的soname设置为libchap14.so.1.0.1,可以通过readelf -d命令来查看(readelf是读取ELF可执行文件或共享库文件的信息的工具)。

    在共享库设置了soname之后,同样使用-lchap14选项编译生成的externtest可执行文件将直接依赖libchap14.so.1.0.1而非libchap14.so,externtest运行时将直接加载libchap14.so.1.0.1文件,可以通过上面的ldd externtest命令或readelf -d externtest命令来查看到,还可以通过将LD_DEBUG环境变量临时设置为libs来查看externtest具体运行时,实际加载的共享库文件名。

    如果没有soname的话,externtest默认只会加载libchap14.so,这样就会失去一定的灵活性,有了soname,就可以灵活的进行共享库的版本控制,比如,我们可以将libchap14.so.1.0.1的soname设置为libchap14.so.1,即在soname里只保留一个主版本号 ---- "1" (我们假定该共享库提供的函数接口在同一个主版本号内是不存在兼容问题的),然后我们创建一个libchap14.so.1的符号链接,让其指向libchap14.so.1.0.1,那么所有链接该共享库的可执行文件都会去加载libchap14.so.1,然后由该符号链接加载对应的libchap14.so.1.0.1,当libchap14.so.1.0.1升级到libchap14.so.1.2.0版本时,只需将libchap14.so.1重新指向libchap14.so.1.2.0即可升级到"2"号次版本。

    如果是libchap14.so.2.0.1的版本(我们假定主版本号"2"与主版本号"1"存在API的兼容问题),那么我们就可以将libchap14.so.2.0.1的soname设置为libchap14.so.2,并创建libchap14.so.2的符号链接,让其指向实际的libchap14.so.2.0.1,然后依赖该共享库的可执行文件,执行时就会加载libchap14.so.2,由于是符号链接,所以实际加载的就会是libchap14.so.2.0.1文件。

    这样,所有依赖主版本号1的可执行文件将只会去加载libchap14.so.1,所有依赖主版本号2的可执行文件将只会去加载libchap14.so.2,从而解决版本的兼容问题:

$ gcc -shared -Wl,-soname,libchap14.so.1 -o libchap14.so.1.0.1 square.o areafunc.o cpuidfunc.o
$ readelf -d libchap14.so.1.0.1
Dynamic section at offset 0x4bc contains 22 entries:
  Tag        Type                         Name/Value
 0x00000001 (NEEDED)                     Shared library: [libc.so.6]
 0x0000000e (SONAME)                     Library soname: [libchap14.so.1]
 .........................................
 .........................................
$ ln -s libchap14.so.1.0.1 libchap14.so.1
$ rm libchap14.so -v
已删除"libchap14.so"
$ ln -s libchap14.so.1.0.1 libchap14.so
$ ls -l
总用量 152
 .........................................
 .........................................
lrwxrwxrwx 1 root root   18  5月  4 21:07 libchap14.so -> libchap14.so.1.0.1*
lrwxrwxrwx 1 root root   18  5月  4 23:08 libchap14.so.1 -> libchap14.so.1.0.1*
-rwxr-xr-x 1 root root 3924  5月  4 23:08 libchap14.so.1.0.1*
 .........................................
 .........................................
$ g++ -o externforV1 externtest.cpp -L. -lchap14
$ LD_LIBRARY_PATH=. ldd externforV1
	linux-gate.so.1 =>  (0xffffe000)
	libchap14.so.1 => ./libchap14.so.1 (0xb76e8000)
	libstdc++.so.6 => /usr/lib/libstdc++.so.6 (0xb75d7000)
	libm.so.6 => /lib/libm.so.6 (0xb75b1000)
	libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0xb7595000)
	libc.so.6 => /lib/libc.so.6 (0xb7431000)
	/lib/ld-linux.so.2 (0xb76eb000)
$ LD_LIBRARY_PATH=. ./externforV1
The radius squared is 100
The area is 314.159
The CPUID is GenuineIntel
$ gcc -shared -Wl,-soname,libchap14.so.2 -o libchap14.so.2.0.1 square.o areafunc.o cpuidfunc.o
$ ln -s libchap14.so.2.0.1 libchap14.so.2
$ rm libchap14.so -v
已删除"libchap14.so"
$ ln -s libchap14.so.2.0.1 libchap14.so
$ ls -l
总用量 164
 .........................................
 .........................................
lrwxrwxrwx 1 root root   18  5月  4 23:12 libchap14.so -> libchap14.so.2.0.1*
lrwxrwxrwx 1 root root   18  5月  4 23:08 libchap14.so.1 -> libchap14.so.1.0.1*
-rwxr-xr-x 1 root root 3924  5月  4 23:08 libchap14.so.1.0.1*
lrwxrwxrwx 1 root root   18  5月  4 23:11 libchap14.so.2 -> libchap14.so.2.0.1*
-rwxr-xr-x 1 root root 3924  5月  4 23:11 libchap14.so.2.0.1*
 .........................................
 .........................................
$ g++ -o externforV2 externtest.cpp -L. -lchap14
$ LD_LIBRARY_PATH=. ldd externforV2
	linux-gate.so.1 =>  (0xffffe000)
	libchap14.so.2 => ./libchap14.so.2 (0xb78cb000)
	libstdc++.so.6 => /usr/lib/libstdc++.so.6 (0xb77ba000)
	libm.so.6 => /lib/libm.so.6 (0xb7794000)
	libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0xb7778000)
	libc.so.6 => /lib/libc.so.6 (0xb7614000)
	/lib/ld-linux.so.2 (0xb78ce000)
$ LD_LIBRARY_PATH=. ./externforV2
The radius squared is 100
The area is 314.159
The CPUID is GenuineIntel
$ 


    从上面的输出可以看到,虽然externforV1与externforV2两个可执行文件在编译时,都使用的是-lchap14,但是在生成externforV1时,libchap14.so符号链接指向的是libchap14.so.1.0.1,而libchap14.so.1.0.1的soname被设置为了libchap14.so.1,所以ldd externforV1查看到externforV1可执行文件在执行时会去加载libchap14.so.1,而libchap14.so.1是指向libchap14.so.1.0.1的符号链接,所以最终加载的就是libchap14.so.1.0.1文件,因此在这里,libchap14.so是linkname(给编译器和链接器看的),libchap14.so.1是soname(可执行文件要加载的),libchap14.so.1.0.1是realname(符号链接最终指向的实际的共享库文件)。

    对于externforV2可执行文件,在g++链接前,先将libchap14.so符号链接指向libchap14.so.2.0.1,而libchap14.so.2.0.1的soname被设置为了libchap14.so.2,所以g++生成的externforV2可执行文件在执行时将去加载libchap14.so.2,而libchap14.so.2是指向libchap14.so.2.0.1的符号链接,所以最终加载的就是libchap14.so.2.0.1,所以在这里,libchap14.so依然是linkname,libchap14.so.2是soname,libchap14.so.2.0.1是realname 。

    很多可执行文件里都会加载一个libc.so.6的共享库(例如上面的externtest程式),该共享库是标准的C库(里面有printf等标准的C库函数),但是libc.so.6并非共享库的realname(实际的共享库名),libc.so.6只是一个soname,并且在lib目录里有一个libc.so.6的符号链接,该符号链接指向实际的共享库文件 ---- libc-2.13.so :

# ls -l /lib/libc.so.6
lrwxrwxrwx 1 root root 12  8月 10  2013 /lib/libc.so.6 -> libc-2.13.so*
# readelf -d /lib/libc-2.13.so

Dynamic section at offset 0x15dd7c contains 26 entries:
  Tag        Type                         Name/Value
 0x00000001 (NEEDED)                     Shared library: [ld-linux.so.2]
 0x0000000e (SONAME)                     Library soname: [libc.so.6]


    所以libc-2.13.so才是realname,libc.so.6只是中间的soname,soname可以方便次版本的升级,同时又与其他的主版本号区分开来。

Debugging Assembly Functions 调试汇编函数:

[zengl pagebreak]

Debugging Assembly Functions 调试汇编函数:

    在之前的很多文章中,都用到了gdb来调试汇编程式,如果想了解gdb的命令行格式和详细的命令行参数的作用,则可以访问之前的"汇编开发相关工具 (二)"里的文章。

    下面就对C或C++程式里调用汇编模块函数的方式进行介绍。

Debugging C programs 调试C程式:

    先来看下需要调试的汇编程式与C程式的源代码:

# square.s - An example of a function that returns an integer value
.type square, @function
.globl square
square:
	pushl %ebp
	movl %esp, %ebp
	movl 8(%ebp), %eax
	imull %eax, %eax
	movl %ebp, %esp
	popl %ebp
	ret

   
/* inttest.c - An example of returning an integer value */
#include <stdio.h>

int main()
{
	int i = 2;
	int j = square(i);
	printf("The square of %d is %d\n", i, j);

	j = square(10);
	printf("The square of 10 is %d\n", j);
	return 0;
}


    上面的inttest.c程式依赖于square.s里的square汇编函数,如果要对inttest.c程式进行调试的话,同样只需在gcc的命令行参数中加入-gstabs参数即可(gcc的标准调试参数是-g参数,但是在Linux环境下,为了能获得更多的调试信息,最好使用-gstabs参数):

$ gcc -gstabs -o inttest inttest.c square.s
$ ./inttest

The square of 2 is 4
The square of 10 is 100

$

    上面通过gcc加-gstabs参数创建了inttest可执行文件,同时将C程式的调试信息添加到了可执行文件里,这样接下来,就可以使用gdb工具来进行调试了:

$ gdb -q inttest
Reading symbols from /root/asm_example/asmfunc/inttest...done.
(gdb) l
1	/* inttest.c - An example of returning an integer value */
2	#include <stdio.h>
3	
4	int main()
5	{
6		int i = 2;
7		int j = square(i);
8		printf("The square of %d is %d\n", i, j);
9	
10		j = square(10);
(gdb) l
11		printf("The square of 10 is %d\n", j);
12		return 0;
13	}
14	
(gdb) 


    在gdb调试器中可以使用"l"命令来查看源代码,如果要设置断点的话,可以直接使用函数名或者行号:

(gdb) break *main
Breakpoint 1 at 0x8048384: file inttest.c, line 5.
(gdb) info b
Num     Type           Disp Enb Address    What
1       breakpoint     keep y   0x08048384 in main at inttest.c:5
(gdb) info break
Num     Type           Disp Enb Address    What
1       breakpoint     keep y   0x08048384 in main at inttest.c:5
(gdb) 


    上面使用break命令在main函数的入口处设置了一个断点,可以通过info b或info break命令来查看当前设置的断点情况。

    设置好断点后,就可以使用run命令来运行程式:

(gdb) run
Starting program: /root/asm_example/asmfunc/inttest 

Breakpoint 1, main () at inttest.c:5
5	{
(gdb) s
main () at inttest.c:6
6		int i = 2;
(gdb) s
7		int j = square(i);
(gdb) s
square () at square.s:5
5		pushl %ebp
(gdb) s
6		movl %esp, %ebp
(gdb) finish 
Run till exit from #0  square () at square.s:6
0x080483a7 in main () at inttest.c:7
7		int j = square(i);
(gdb) s
8		printf("The square of %d is %d\n", i, j);
(gdb) print j
$2 = 4
(gdb) c
Continuing.
The square of 2 is 4
The square of 10 is 100

Program exited normally.
(gdb) q
$


    由于之前在main函数的入口处设置了断点,因此,run启动程式后,程式就会在main函数的入口处中断下来,然后我们就可以使用s命令来单步执行语句,当遇到square汇编函数时,s命令还可以追踪进square函数的内部,即square.s的源代码里,然后就可以调试汇编函数了,如果想直接结束函数返回,可以使用上面用到的finish命令,该命令会执行完square函数,同时返回到main主函数。

    如果需要查看变量信息,可以使用print命令,例如上面的print j命令就可以将变量j的值给输出显示出来,通过c命令继续执行后面的语句,在程式执行完后,就可以通过q命令来退出gdb 。

    在单步执行语句时,有时候你可能不希望追踪进函数,那么就可以使用next命令:

(gdb) run
Starting program: /root/asm_example/asmfunc/inttest 

Breakpoint 1, main () at inttest.c:5
5	{
(gdb) s
main () at inttest.c:6
6		int i = 2;
(gdb) s
7		int j = square(i);
(gdb) n
8		printf("The square of %d is %d\n", i, j);
(gdb) n
The square of 2 is 4
10		j = square(10);
(gdb) next
11		printf("The square of 10 is %d\n", j);
(gdb) 


    上面使用n或next命令以单步步过的方式执行完square函数,这种方式下,不会追踪进入函数。

    如果不需要调试信息,则只需在编译时,将-gstabs参数去掉即可,就可以生成发布版的程式了。

    最后是原著的一些关于第14章的总结内容,限于篇幅就不多说了。

    下一篇开始将介绍和编译优化有关的内容。

    OK,就到这里,休息,休息一下 o(∩_∩)o~~
上下篇

下一篇: 优化汇编指令 (一)

上一篇: 调用汇编模块里的函数 (二)

相关文章

汇编函数的定义和使用 (一)

Moving Data 汇编数据移动 (四) 结束篇 栈操作

高级数学运算 (二) 基础浮点运算

汇编函数的定义和使用 (二)

优化汇编指令 (三)

IA-32平台(一)