浅谈编译过程
程序的编译经历这几个过程
预处理器是用来进行语法检测的,链接器是用来将各个可执行文件链接起来,大致可以理解为给各个子函数提供一个工作流程以及一个主函数入口(这里的子函数包括printf那些系统提供的函数)这个可执行文件会记录各个对象文件(.obj)的调用关系,调用路径,也有可能会记录执行这个文件需要的配置信息(比如需要的内存空间信息)。
关于最后这条信息,我要专门说明一下,我们在自己测试鲁棒性水印算法的时候发现,因为鲁棒性水印算法的工作流程决定了算法需要比较庞大的空间,超过了系统默认的分配空间,所以我第一次跑的时候直接就stack overflow了,当我们自己修改了visual studio里面的配置,手动添加堆栈的空间,这个爆栈的问题就解决了(项目->属性->链接器->系统->修改堆栈保留大小以及堆栈提交大小)。作为对比,我们将算法流程打包成一个动态链接库,然后在动态链接库里面添加堆栈大小,然后用别的程序调用这个动态链接库,但是爆栈了,但是如果使用c++调用,在调用的时候将空间大小写死就不会出现这个问题。对于这个问题我有两个猜测,(既然是猜测那就还没有验证)第一个猜测是,生成可执行文件的时候会有配置文件,生成配置文件这个步骤是在链接器那一步执行的,所以在对于动态链接库就不会有这种配置文件,这样显然,动态链接库能使用多少空间完全取决于调用的程序,需要修改调用这个库的函数的大小。第二个猜测就是,是我太菜,动态链接库也是有这种配置方式的,是我没有配置好。
接着说库的问题,从这个工作流程可以看到,对于一个完整的项目来说,如果需要多次调用某个函数,就需要把这个函数的obj文件拷贝多份,每次函数调用的时候都需要在内存中重新开辟一个新的空间,这无疑加大了程序的负担,所以就有了动态链接库这个概念。
动态链接库这个东西在Windows平台下叫动态链接库(dynamic link library),后缀名为
在linux平台下叫共享对象(shared object),后缀名为
,从字面上理解的话,共享对象。调用动态链接库有两种方式,第一种方式为一个库的代码,只要运行过一次,便会占用物理内存,直到进程终止。第二种方式为库代码占用的内存可以通过dlfree的方式释放掉,返回给物理内存。差别主要是对于那些寿命很长,但又偶尔调用各种库的进程,建议使用第二种调用方式。
静态链接库和动态链接库的关系可以理解为头文件和函数主体的关系,比如我们现在有个头文件
//文件名为 test.h
#include <iostream>
int add(int a,int b);
同时又有个同名的c文件
//文件名为 test.c
#include <iostream>
#include "test.h"
int add(int a,int b){
return a+b;
}
我们使用动态链接库的时候就好像只引用了头文件,在引用的位置有一个符号表,编译的时候只将符号表编译进去了,这样如果函数本体十分巨大的话,就可以很大程度上缩减代码的体积,当我们需要用到这个函数的时候,再由符号表去链接我们的动态链接库(调用方式上面已经提到过了,并且库文件本来就是一个可执行的二进制文件,不需要再编译)。这样做还有个好处就是,当我们需要修改函数的内容的时候,我们只需要重新编译这个库就行,不需要再将项目重新编译。在windows平台下,静态链接库和我刚刚说的那个符号表文件都叫
文件(这个符号表的叫法是我自己取的,字面翻译应该叫库文件),静态链接库在windows平台下就好像是将函数的本体直接写在了头文件下,项目编译的时候直接将整体代码拷贝过去参与编译。(linux平台下静态链接库为
文件)。
在windows平台下,动态链接库的寻址方式有5种,
1,可执行文件目录下
2,当前目录下(比如,我在B盘调用A盘的可执行文件,当前目录就是B盘,可执行文件目录就是A盘)
3,系统环境变量,(就是我的电脑右键,属性,Advanced System Setting,environment variable,PATH)
4,System32文件夹
5,可执行文件编译的时候指定的文件夹
对于第五种寻址的方式,对于动态链接库链接动态链接库不可用,因为编译动态链接库的时候只有符号表参与了编译,最后的如果指定路径,那是最后链接器的步骤,但是显然,编译动态链接库没有那一步,只能自动寻址,这就是为啥别的语言调用我那个算法的动态链接库的时候必须把傅里叶变换的库以及把opencv的那个库放到System32或者环境变量下。c++代码调用算法库的时候可以不配环境,但是得把需要的这两个库放在.exe文件的同一个文件夹下。
在linux环境下,寻址方法是大同小异的,这里就不再赘述了。
在Windows环境下,编译动态链接库和编译静态链接库基本上完全一样,都是用visual studio编译,只不过在编译选项选择的时候选择静态或者动态罢了。
一般的库模板如下
头文件 test.h
#pragma once
#ifndef WINDLL_H
#define WINDLL_H
#ifdef __cplusplus
#define EXPORT extern "C" __declspec (dllexport)
#else
#define EXPORT __declspec (dllexport)
#endif
#include <string>
using namespace std;
EXPORT void encode(string param_1,string param_2,string param_3);
#endif
函数体 test.cpp
#include "test.h"
EXPORT void encode(string param_1,string param_2,string param_3){
函数的具体内容
}
其他的参数和编译exe一样。
在Linux下,最常用的有两个编译参数 -fPIC -shared(其他的参数和编译可执行文件一样)
fpic就是 position independent code ,这个编译参数告诉编译器产生位置无关的代码,产生的代码中,没有绝对地址,全部使用相对地址,故代码可以被加载到内存的任意位置都可以被正确执行,共享库在被加载的时候,在内存的位置不是固定的。不加这个参数编译出来的so文件是要再加载时根据加载到的位置再次重定向的(具体参考 https://blog.csdn.net/derkampf/article/details/69660050)
shared就不赘述了,直接看那个链接的博客,那个写的比较详细。
在linux下编写静态库没试过,就不写了,编写动态库的模板如下
extern "C"{
函数体
}
大致就是这个意思,到时候要用的话再去查吧。
最后提一句在Linux下可以通过
指令查看可执行文件链接库的关系。