为什么两段代码都加上-visibility=hidden,普通函数编译错误,而虚函数编译正确?

问题描述 投票:0回答:1

我正在阅读此
我按照答案一步步进行测试,还添加

-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
,普通函数编译错误,而虚函数编译正确?

c++ gcc virtual-functions gcc-attribute
1个回答
0
投票

库有一个包含导出符号的符号列表(即函数和全局变量)。目标文件有一个它使用的符号列表,这些符号未在该目标文件中定义,即导入的符号。

符号上的

隐藏可见性意味着它不会添加到导出符号列表中。 默认可见性意味着它是。

当链接器从一堆目标文件创建库/可执行文件时,它会查看所有目标文件的导入符号,并根据所有目标文件和库的导出符号来解析它们。

让我们看第一个例子。您的 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。此外,由于编译器可能会对调用进行去虚拟化,因此当它尝试通常的直接调用时,您还会再次收到这些符号的错误。

© www.soinside.com 2019 - 2024. All rights reserved.