1. 先写好咱们的示例代码
// main.cpp
#include "Scheduler.h"
int main(int argc, char const *argv[])
{
Scheduler::instance().run();
return 0;
}
// runner.cpp
#include "Scheduler.h"
#include
static struct Runner {
Runner() {
Scheduler::instance().register_function([]() {
std::cout << "This is runner stdout.\n";
});
};
} register_runner;
// Scheduler.h
#pragma once
#include
#include
class Scheduler {
public:
static Scheduler& instance() {
static Scheduler scheduler;
return scheduler;
}
void register_function(std::function f) {
runners.push_back(f);
}
void run() {
for (auto& r : runners) {
r();
}
}
private:
std::vector<std::function> runners;
Scheduler() = default;
};
对以上代码做个说明:
Scheduler是一个调度器,它是单例模式;在main.cpp里会获取Scheduler的实例,然后调用运行,实例会调用已经注册好的运行器;在这个过程中main.cpp不知道运行器代码的存在,调度器也不知道有那些运行器。
程序期望在启动的时候,运行器自动注册到调度器中,主程序运行调度程序,调度程序运行所有已注册运行器。
Runner.cpp中的register_runner是一个静态全局变量,在Runner.cpp被加载的时候进行初始化,调用构造函数,把function注册到Scheduler。这种方式经常用于插件开发。
Makefile文件内容如下:
CXX = g++
CFLAGS = -std=c++17 -Wall
all: static dyn
static:
$(CXX) $(CFLAGS) -c runner.cpp
ar rcs runner.a runner.o
$(CXX) $(CFLAGS) main.cpp -o main_static runner.a
rm runner.a
dyn:
$(CXX) $(CFLAGS) -fPIC -c runner.cpp
$(CXX) -shared -o librunner.so runner.o
$(CXX) $(CFLAGS) main.cpp -o main_dyn -L. -lrunner
clean:
rm -f *.o *.a *.so main_static main_dyn main
编译成动态库和静态库的主要目的就是看看动态库和静态库之间有什么区别。
编译后执行:
./main_dyn # 运行前修改配置LD_LIBRARY_PATH,加入当前路径
./main_static
按预期,都应该打印出:
This is runner stdout.
但是运行后并没有任何输出。
2. 检查问题出现在哪里?
查询资料后发现是链接器优化了代码,把没有用到的符号不进行链接,节省程序符号空间,Runner.a或Runner.so都没有被其它代码引用,所以被优化掉了,因为我们要做的是Runner初始化的时候注册函数,所以需要Runner.a[so]种的符号不论什么情况都需要被执行。修改Makefile如下:
CXX = g++
CFLAGS = -std=c++17 -Wall
all: static dyn
static:
$(CXX) $(CFLAGS) -c runner.cpp
ar rcs runner.a runner.o
$(CXX) $(CFLAGS) main.cpp -o main_static -Wl,--whole-archive runner.a -Wl,--no-whole-archive
rm runner.a
dyn:
$(CXX) $(CFLAGS) -fPIC -c runner.cpp
$(CXX) -shared -Wl,--no-as-needed -o librunner.so runner.o
$(CXX) $(CFLAGS) main.cpp -o main_dyn -L. -Wl,--no-as-needed -lrunner
clean:
rm -f *.o *.a *.so main_static main_dyn main
静态库需要增加编译参数-Wl,--whole-archive和-Wl,--no-whole-archive,而且它们必须成对出现,把需要的静态库放在它们中间。-Wl,--whole-archive告诉编译器就算符号没有被调用,也要全部打包进行编译;如果以上两个参数没有成对出现,编译会报错。
动态库需要增加编译参数-Wl,--no-as-needed,告诉编译器就算符号没有被调用,也要全部打包进行编译。
这样修改编译参数,进行编译后,运行没有问题。
3. 另外一个问题
main.cpp和Runner.cpp都包含了Scheduler.h,按正常理解,头文件会在两个.cpp文件中展开,为什么它们会得到同一个实例,而不是两个实例,链接器在这里面起了什么作用?经过一番查询,得到了下面结论:
编译过程分析
1. 预处理阶段 main.cpp 和 runner.cpp 都包含了 Scheduler.h 预处理器将头文件内容复制到两个源文件中
2. 编译阶段
编译器为每个源文件生成目标文件:
main.o: 包含对 Scheduler::instance() 的调用 包含静态局部变量的初始化代码
runner.o: 也包含对 Scheduler::instance() 的调用 也包含静态局部变量的初始化代码
3. 链接阶段(关键)
链接器在这里扮演了关键角色:
弱符号处理: 静态局部变量的实现使用了一个guard变量 这个guard变量被标记为弱符号(weak symbol) 链接器会将所有相同的弱符号合并为一个
符号解析:
.data.Scheduler::instance()::scheduler // 这是静态变量的存储
.bss.Scheduler::instance()::guard // 这是控制初始化的guard变量
地址绑定: 所有对 Scheduler::instance() 的调用被解析到同一个函数实现 该函数内部的静态变量被绑定到同一个内存位置
4. 结论
这种开发方式经常被用在插件开发中。总结一下在开发中经常可能会遇到的问题。