我正在阅读此页。
我按照答案一步步进行测试,还添加
-fvisibility=hidden
以隐藏所有符号,然后我扩展了答案中的代码。
//rectangle.h
#pragma once
#include <memory>
class Rectangle {
int width, height;
public:
void set_values (int,int);
int area();
};
std::shared_ptr<Rectangle> __attribute__((visibility("default"))) get_rectangle();
//rectangle.cpp
#include "rectangle.h"
int Rectangle::area() {return width*height;}
void Rectangle::set_values (int x, int y) {
width = x;
height = y;
}
std::shared_ptr<Rectangle> __attribute__((visibility("default"))) get_rectangle() {
return std::make_shared<Rectangle>();
}
我构建 librectangle.so,然后编写 main.cpp 来链接 librectangle.so
//main.cpp
#include "../rectangle/rectangle.h"
int main() {
auto rectangle = get_rectangle();
rectangle->set_values(2, 3);
rectangle->area();
return 0;
}
编译错误
undefined reference to `Rectangle::set_values(int, int)'
undefined reference to `Rectangle::area()'
但是,如果我将类成员函数更改为虚函数,它将正确编译
//changed rectangle.h
#pragma once
#include <memory>
class Rectangle {
int width, height;
public:
virtual void set_values (int,int);
virtual int area();
};
std::shared_ptr<Rectangle> __attribute__((visibility("default"))) get_rectangle();
使用
Use
nm -C librectangle.so
来比较普通函数类和虚函数类。左边是普通的功能类。右边是虚函数类。
nm -C librectangle.so | grep Rectangle
nm -C librectangle.so | grep area
nm -C librectangle.so | grep set_values
两个demo都加了
-visibility=hidden
,普通函数编译错误,而虚函数编译正确?
库有一个包含导出符号的符号列表(即函数和全局变量)。目标文件有一个它使用的符号列表,这些符号未在该目标文件中定义,即导入的符号。
符号上的隐藏可见性意味着它不会添加到导出符号列表中。 默认可见性意味着它是。
当链接器从一堆目标文件创建库/可执行文件时,它会查看所有目标文件的导入符号,并根据所有目标文件和库的导出符号来解析它们。
让我们看第一个例子。您的 main.o 被编译为大致达到此效果的程序集(假设除了堆栈指针之外没有被调用者保存的寄存器):
main:
sub sp, 16 ; make space for shared_ptr on stack
mov arg0, sp ; move address of that space to first argument register
call get_rectangle
mov arg0, [esp] ; load pointer to object behind shared_ptr into argument
mov arg1, 2 ; load arguments
mov arg2, 3
call Rectangle::set_values
mov arg0, [sp] ; load pointer again
call Rectangle::area
mov arg0, sp ; load shared_ptr address
call shared_ptr::~shared_ptr ; destroy shared_ptr, end of scope
如您所见,它引用了三个使用的符号:
get_rectangle
、Rectangle::set_values
和Rectangle::area
。但只有 get_rectangle
具有默认可见性,因此其他两个未在 librectangle.so 中列为导出,因此链接器无法找到它们。你会得到错误。
但这不是虚拟通话的工作原理。代码看起来像这样:
main:
sub sp, 16 ; as before
mov arg0, sp
call get_rectangle
mov arg0, [esp] ; load pointer to object behind shared_ptr
mov reg0, [arg0] ; load vtable pointer to call virtual function
mov arg1, 2 ; load arguments
mov arg2, 3
call [reg0+0] ; call first entry in vtable, set_values
mov arg0, [esp] ; load pointer again
mov reg0, [arg0] ; load vtable pointer again
call [reg0+8] ; call second entry in vtable, area
如您所见,现在程序集仅引用单个符号,
get_rectangle
。其他的仅通过存储在vtable中的指针来调用。 vtable是存储在librectangle.so中的全局常量,因此它可以访问隐藏函数。另外,vtable本身是一个隐藏符号,但它仅被Rectangle
的构造函数引用,这是由编译器隐式定义的。它是一个内联函数,因此存在于包含标头的每个编译单元中,但唯一使用它的地方是在 get_rectangle
中,因此它可以访问 vtable 符号。
请注意,如果您在
Rectangle
中创建类型为 main
的局部变量,您将收到一个链接器错误,指出缺少 vtable。此外,由于编译器可能会对调用进行去虚拟化,因此当它尝试通常的直接调用时,您还会再次收到这些符号的错误。