Linux的编译知识

Posted ~Old

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux的编译知识相关的知识,希望对你有一定的参考价值。

本地编译

本地编译是指在Linux操作系统上使用与该操作系统相同的CPU架构进行编译。在本地编译中,开发人员可以直接使用GCC等编译器进行编译,非常方便快捷。

编译一个hello world程序的示例命令为:

gcc -o hello hello.c

这将会生成一个可执行文件 hello,可以使用下面的命令来运行程序:

./hello

交叉编译

交叉编译是指使用一台与目标系统不同的计算机(比如使用x86架构的计算机编译ARM架构的代码),通过交叉编译工具链进行编译生成可在目标系统上运行的程序。

交叉编译的优势在于可以在速度较快的开发计算机上进行编译,生成针对目标系统的可执行文件,以节省目标系统上的时间和资源。

编译一个ARM架构的hello world程序的示例命令为:

arm-linux-gnueabi-gcc -o hello_arm hello.c

这将会生成一个针对ARM架构的可执行文件 hello_arm,可以将该文件复制到目标系统上运行。

安装arm架构的gcc命令为

如果你的 Linux 系统上没有安装 arm-linux-gnueabihf-gcc,可以按照以下步骤进行安装

1、打开终端,并使用 apt-get 命令更新软件包列表:

sudo apt-get update

2、然后使用 apt-get 命令安装 arm-linux-gnueabihf-gcc

sudo apt-get install gcc-arm-linux-gnueabihf

一个C程序在Linux下的4个阶段

在Linux下,一个C程序的编译过程通常可以分为以下4个阶段:

预处理

预处理是指在编译过程中对源代码进行预处理。主要的任务是将源代码中的宏定义、条件编译等预处理指令进行展开,并将结果保存到一个中间文件中。预处理结果包括去掉注释、展开宏定义、加入头文件等。

预处理的命令是 gcc -E,它将源文件进行预处理后输出到标准输出,我们可以使用重定向将其保存到一个文件中。例如,下面的命令将预处理结果保存到hello.i文件中:

gcc -E hello.c -o hello.i

编译

编译是指将预处理后的代码翻译成汇编代码,同时对代码进行语法检查、类型检查等操作,生成目标文件。目标文件是一种机器码的二进制文件,其中包含了可执行代码和符号表等信息。

编译的命令是 gcc -S,它将源文件编译成汇编代码,也可以使用 -c 选项生成目标文件。例如,下面的命令将生成汇编代码文件hello.s:

gcc -S hello.c -o hello.s

汇编

汇编是指将编译生成的汇编代码翻译成机器码,生成二进制目标文件。汇编器根据汇编代码中的伪指令和操作码来生成机器码。

汇编的命令是 gcc -c,它将汇编代码转换为机器码生成目标文件,例如:

gcc -c hello.s -o hello.o

链接

链接是将多个目标文件合并成一个可执行文件,同时解析符号引用关系并进行重定位,生成可在系统上运行的二进制文件。

链接的命令是 gcc,它将多个目标文件链接为一个可执行文件,例如:

gcc hello.o -o hello

其中,hello.o 是目标文件,hello 是可执行文件。在链接过程中,链接器会将 hello.o 中的符号引用解析为实际的地址,并将其与其他库文件链接起来生成可执行文件 hello。

通过这4个阶段,我们可以将C程序编译成可执行文件并在Linux系统上运行。

Linux动态库相关知识整理

动态库和静态库在C/C++开发中很常见,相比静态库直接被编译到可执行程序, 动态库运行时加载使得可执行程序的体积更小,更新动态库可以不用重新编译可执 行程序等诸多好处。作者是一个Linux后台开发,这些知识经常用到,所以 整理了一下这方面的知识。静态库相对简单,本文只关心Linux平台下的动态库。


创建动态库


这里我把一个短小却很有用的哈希函数编译成动态库做为示例,ELFhash用于对字符串做哈希,返回一个无符号整数。


    //elfhash.h

    #include <stdio.h>

    unsigned long  ELFhash(const char* key);


    //http://www.qixoo.qixoo.com/elfhash.c

    #include "elfhash.h"

    unsigned long ELFhash(const char* key)  

    {

      unsigned long h = 0, g;

      while( *key ) {

        h = ( h << 4 ) + *key++;

        if ( (g = h & 0xF0000000) != 0 )

           h ^= g >> 24;

        h &= ~g;

    }

      return h;

    }


接下来使用gcc编译以上代码,并用ld将编译的目标文件链接成动态库


    gcc -fPIC -c -Wall elfhash.c  

    ld  -shared elfhash.o -o libelfhash.so  


其中-fPIC意思是生成位置无关的代码(Position Independent Code),适用于动态库,在多个进程中共享动态库的同一份代码。ld的-shared选项告诉链接器创建的是动态库。gcc也可以间接调用ld生成动态库


    gcc -fPIC -shared -Wall -o libelfhash.so elfhash.c  


使用动态库


动态库的使用方法有两种一种是隐式使用,第二种是显式使用。隐式使用的方法很简单。


    #include "elfhash.h"

    int main()  

    {

        printf("%ld\n", ElfHash("key-for-test"));

        return 0;

    }

显式使用动态库需要借助以下几个函数


    #include <dlfcn.h>

    void *dlopen(const char *filename, int flag); //flag可以是RTLD_LAZY,执行共享库中的代码时解决未定义符号,RTLD_NOW则是dlopen返回前解决未定义符号。  

    char *dlerror(void); //当发生错误时,返回错误信息  

    void *dlsym(void *handle, const char *symbol); //获取符号  

    int dlclose(void *handle); //关闭  


应用上面几个函数,调用ELFhash实现跟隐式调用一样的功能


    #include "elfhash.h"

    #include <stdlib.h>

    #include <dlfcn.h>


    int main() {  

        void *handle;

        unsigned long (*hash)(const char*);

        char *error;

        handle = dlopen ("./libelfhash.so", RTLD_LAZY);

        if (!handle) {

            fputs (dlerror(), stderr);

            exit(1);

        }

        hash = dlsym(handle, "ElfHash");

        if ((error = dlerror()) != NULL)  {

             fputs(error, stderr);

             exit(1);

        }

        printf ("%ld\n", (*hash)("key-for-test"));

        dlclose(handle);

    }


至此了解以上的知识就可以创建和使用动态库了。 实际应用中我们可能还是会遇到一些问题。


动态库的加载


动态库创建那一节,我演示如何隐式使用动态库,那么编译运行这段代码试一下。


    gcc main.c -L./ -lelfhash  

    ./a.out //执行可执行程序

    //以下是输出结果

    ./a.out: error while loading shared libraries: libelfhash.so: cannot open shared object file: No such file or directory


结果运行时报错,可执行程序找不到动态库。 网上有一些说法是编译时设置-L选项,但在Linux上面证明是不行的(SunOS上可行),这个选项只能在编译链接时有效, 可以让你使用-l如上面的-lelfhash。使用readelf -d a.out可以看到可执行文件依赖的动态库信息。


     0x0000000000000001 (NEEDED)  Shared library: [libelfhash.so]


可以看到这里面并没有包含动态库的路径信息。查阅一下动态链接器的文档man ld-linux.so可以发现这样一句话(有的没有,版本问题)


    If a slash is found, then the dependency string is interpreted as a (relative or absolute) pathname, and the library is loaded using that pathname


这段话太长,我只截取一部分,大致就是说,当依赖中有/符号,那么会被解析成动态库加载的路径,隐式使用的例子换一种编译方法。


    gcc main.c ./libelfhash.so  

    ./a.out

    23621492 //输出正常  


再用readelf -d a.out查看会发现,依赖信息中有了一个路径。


    0x0000000000000001 (NEEDED)  Shared library: [./libelfhash.so]  


这种方法虽然解决了问题,但是依赖中的路径是硬编码,不是很灵活。 动态链接器是如何查找的动态库的需要进一步查阅文档。关于查找的顺序有点长,这里就不直接引用了,大致是这样:


1、(仅ELF文件) 使用可执行文件中DT_RPATH区域设置的属性,如果DT_RUNPATH被设置,那么忽略DT_RPATH(在我的Linux对应的是RPATH和RUNPATH)。

2、使用环境变量LD_LIBRARY_PATH,如果可执行文件中有set-user-id/set-group-id, 会被忽略。

3、(仅ELF文件) 使用可执行文件中DT_RUNPATH区域设置的属性

4、从/etc/ld.so.cache缓存文件中查找

5、从默认路径/lib, /usr/lib文件目录中查找


我们需要设置RPATH或者RUNPATH,可以这样做


    gcc main.c -Wl,-rpath,/home/xxx,--enable-new-dtags -L./  -lelfhash  


这里的-Wl选项告诉链接器ld如果如何处理,接下来传递的-rpath(或者使用-R)告诉ld动态库的路径信息(注意-Wl,和后面选项之间不能有空格)。如果没有--enable-new-dtags那么只会设置RPATH,反之,RPATH和RUNPATH会同时被设置。使用readelf -d a.out查看结果:


    0x000000000000000f (RPATH)  Library rpath: [/home/xxx]  

    0x000000000000001d (RUNPATH)  Library runpath: [/home/xxx]  


如果使用环境变量LD_LIBRARY_PATH,那么一般这样用 export


    export LD_LIBRARY_PATH=/home/xxx;$LD_LIBRARY_PATH  


RPATH和RUNPATH指定动态库的路径,用起来简单,但是也缺乏灵活性,LDLIBRARYPATH在临时测试的也是很有用的,但是在正式环境中,直接使用它也不是好的实践,因为环境变量跟用户的环境关系比较大。动态库不仅要考虑自己使用, 还有分发给别的用户使用的情况。


更通用的方法是使用ldconfig,有几种方法,先在/etc/ld.so.conf.d/目录下创建一个文件,然后把你的动态库路径写进去。或者将你的动态库放到/lib,/lib64(64位),/usr/lib,/usr/lib64(64位)然后运行sudo ldconfig重建/etc/ld.so.cache文件。


动态库版本


通常在使用第三方给的动态库的时候,都是带有版本(文件命名),可以在/usr/lib64下看到很多这样的动态库。现在我重新编译动态库,这次加上版本信息。


    gcc -fPIC -shared -Wall -Wl,-soname,libelfhash.so.0  -o libelfhash.so.0.0.0 elfhash.c  


每个动态库都有一个名字,如这里的libelfhash.so.0.0.0,叫real name,命名规则跟简单,通常是libxxx.so.MAJOR.MINOR.VERSION(有的时候VERSION会被省略),如果动态库在接口上的兼容性,比如删除了接口或者修改了接口参数,MAJOR增加,如果接口兼容,只是做了更新或者bug修复那么MINOR和VERSION增加。也就是说MAJOR相同的库接口都是兼容的,反之不兼容,如果使用不兼容的动态库需要重新编译可执行程序。


编译动态库时,通过给ld传递连接选项-soname可以指定一个soname, 如这里的libelfhash.so.0 只保留MAJOR,可执行程序运行加载动态库时,会加载这个指定名字的库。


动态库还有一个名字是link name,编译可执行程序时,传个链接器ld的动态库名字,通常是没有版本号以.so结尾的文件名。 一般作法是对soname创建软链。


按照这个规则来命名的动态库可以ldconfig识别,我们把libelfhash.so.0.0.0放到/usr/lib64文件夹中,执行以下指令


    $sudo ldconfig -v | grep libelfhash.so

    libelfhash.so.0 -> libelfhash.so.0.0.0  


可以发现ldconfig根据libelfhash.so.0.0.0的信息,创建了一个soname指向real name的软链,当动态库更新(MINOR,VERSION增加),拷贝新库到相应的位置,再执行sudo ldconfig会自动更新软链指向最新的动态库,动态库更新就完成了。


总结


OK,关于Linux动态库知识整理就到这里了,这些知识虽说都是些基础,少有涉及动态库内部的一些原理,但是却很常用。整理过程中我带着疑问去阅读了ld和ld-linux.so的文档,收获颇丰。同样,希望本文能帮你解释遇到的部分问题或疑惑。





























































































































































































































































以上是关于Linux的编译知识的主要内容,如果未能解决你的问题,请参考以下文章

linux下 gcc可以编译hello.c arm-linux-gcc 无法找到libz.so.1

gcc和arm-linux-gcc是啥关系?区别是啥?

arm-linux-gcc 常用参数讲解 gcc编译器使用方法

arm-linux-gcc怎么编译自己写的头文件

arm-linux-gcc交叉编译是静态编译还是动态的

请教arm-linux-gcc 版本与QT版本和linux内核的关系!