当前位置:编程学习 > C/C++ >>

杨力祥老师在C++课后给同学留了一道思考题,即探讨C++函数调用时其内存的结构究竟是什么样的。在参考《程序员的自我修养》的过程

本文主要介绍了一下在Linux下开发c/c++时候,不可避免的会开发或者生成.o .a .so这种中间库状态的文件(可能是自己写了一个lib让别人调用,或者提供.c/.cpp文件嵌入别人的Makefile工程)。如何查看这些库文件的一些基本信息。有时候大家编译程序时候(确切的说是链接器链接的时候)很多错误例如"undefine reference",之类的常见错误,原因就是因为没有找到.o .a .so的库文件,导致链接失败。

        -------------------------------------------------------------------------------

        1、Linux库文件

        2、库文件的使用方式

        3、利用tar/nm查看库文件的信息

        -------------------------------------------------------------------------------

       1、库文件的定义之类的就不在此累赘了,有兴趣Google之。说白了就是我们写好一些对应的.h和.c(.cpp)文件,然后通过编译器的编译,生成中间代码供他人使用,他人只需要将你的中间代码include进自己的程序即可。注意,编译器编译成最终可执行的文件需要好几步,基本可以分为:文本解析->语法解析->此法分析->预处理分析->编译->连接。生成中间库是没有链接阶段的,在Linux Gcc下通过-C参数指定只编译不链接,所以如果写了一个.c文件用到了比如pthread_create之类的外部调用,在Gcc -C编译的时候不用-lpthread因为这个时候是不需要链接的。

      2、(对库文件熟悉话直接跳过)在各个系统平台上,库文件的格式和形式各不相同,Windows下就是如同xxx.dll或者xxx.lib,*inux下就是xxx.so或者xxx.a。两种分别对应的是静态库和动态库,静态库会连同编译器编译链接进入程序成为程序的一部分,好处是作为程序的一部分不用每次运行时都去load(弊端是可能很多进程都用到这个库但是每个进程中都有一份,动态库的话内存中只有一份,通过重定向来加载),而且不会导致因库的缺失而运行失败,坏处是会导致可执行文件偏大。动态库是程序运行时动态加载到进程里去的,而且可以多进程共,并且方便软件更新,直接替换老的库即可。

      到底使用静态还是动态库取决于程序的使用上下文环境,一般第三方库都提供了两种版本,系统库的话一般都是动态链接库,因为同样系统下的库都是一样的。用Linux的Gcc举例:

      一、写了一个.h声明一个foo()函数,然后在.c或者.cpp中实现foo()函数

      二、gcc(g++) -c -o foo.o foo.c (注意此处不需要.h头文件,头文件只是对库的对外接口描述)

      三、生成静态库: ar -r libfoo.a foo.o (静态库.a其实就是.o文件的压缩包,注意这里不支持把.a打入.a)

             生成动态库: gcc foo.c -fPIC -shared -o libfoo.so(-fPIC意思是生成位置无关代码,因为动态库是运行时加载的,需要对代码进行重定向,不清楚可以Google一下)

      四、写个含有main函数的文件,并调用foo()函数 : gcc(g++) -o test test.c -lfoo,这里-lfoo意思是去找以lib开头的某.so或.a文件,默认优先找.so动态库。 www.zzzyk.com

      需要注意一下的是,如果是cpp引用了c的库或.c那么头文件里要用externc "C"关键字来指定按c的方式读取(根本上是因为c和c++的函数签名不一致,因为c++支持重载,所以按c++的方式是找不到同名的c函数的)。在使用动态库的情况下,程序回去一些预定义的地方找.so文件。比如/usr/lib/下,如果需要自己指定,请修改/etc/ld.so.conf文件。并用ldconfig来刷新cache。

 

      3、如果我们需要查看自己写的库的信息时可以用nm来查看,如查看库中有哪些函数,有哪些全局变量,有哪些依赖别的库的东西等等,下面我们写一个例子来说明一下:

[cpp] 
#include <stdio.h> 
 
int g1;  
int g2 = 0; 
 
static int g3;  
static int g4=0; 
 
const int g5=0; 
 
static const int g6 = 0; 
 
int main(int argc, char *argv[]) 

    static int st = 0; 
 
    int t1;  
    int t2 = 0; 
 
    const int t3 = 0; 
 
    printf("printf-function"); 
 
    return 0; 

 
void foo1(){} 
static void foo2(){} 
[cpp] view plaincopyprint?
void overload(int i){} 
[cpp] view plaincopyprint?
void overload(float i){} 

        linux的nm命令可以一个文件中的符号列表,列出以上代码Gcc -c编译出的a.o(a.a a.so)可以通过nm命令来查看其中的符号信息:

[cpp]
0000000000000000 t  
0000000000000000 d  
0000000000000000 b  
0000000000000000 r  
0000000000000000 r  
0000000000000000 n  
0000000000000000 n  
0000000000000000 B g1 
0000000000000004 B g2 
0000000000000008 b g3 
000000000000000c b g4 
000000000000001c r g5 
0000000000000020 r g6 
                 U __gxx_personality_v0 
0000000000000000 T main 
0000000000000000 a nm.cpp 
                 U printf 
000000000000003e T _Z4foo1v 
0000000000000044 t _Z4foo2v 
0000000000000054 T _Z8overloadf 
000000000000004a T _Z8overloadi 
0000000000000010 b _ZZ4mainE2st 
        其中左边第一列是符号的地址值,对应源码可以看出递增的规律。第二列是该符号的类型,第三列是符号的名称(比如函数名,变量名):

        符号类型:介绍几个最常用的,其他的如果遇到了直接Google:

           B --- 全局非初始化数据段(BBS段)的符号,其值表示该符号在bss段中的偏移,如g1

           b --- 全局static的符号,如g3

           r --- const型只读的变量(readonly)

           N --- debug用的符号

           T --- 位于代码区的符号,比如本文件里的函数main foo

           t  --- 位于代码区的符号,一般是static函数

           U --- 位于本文件外的调用函数或变量符号,比如系统的printf()函数

       这里要注意的是,本人使用g++编译的,所以是按c++的支持重载的函数风格编译的,可以看到所有函数均带了前缀和后缀,前缀代表属于类的名字,后缀代表参数列表的类型缩写,因为重载必须是区分参数类型,这里也可以看出,为什么返回值不同的函数不是重载,因为符号表里没有返回值的记录。

       例如两个overload函数的后缀分别是f和i代表一个是float型一个是int型(上面还有v ->void型)。

       nm命令对大家调试多模块的程序很有用处,大部分情况下可以解决"undefined reference"的问题,如果大家发现nm的另类很好的用法,也可以留言哈!!!

补充:软件开发 , C++ ,
CopyRight © 2022 站长资源库 编程知识问答 zzzyk.com All Rights Reserved
部分文章来自网络,