【问题标题】:What is the meaning of "i32 (...)**" in LLVM IR?LLVM IR中的“i32(...)**”是什么意思?
【发布时间】:2021-05-21 09:46:33
【问题描述】:

我正在阅读 Clang++ 生成的以下代码的 LLVM IR 代码:

class Shape {
  public:
  // pure virtual function providing interface framework.
  virtual int getArea(char* me) = 0;
  void setWidth(int w) {
    width = w;
  }
  
  void setHeight(int h) {
    height = h;
  }
  
  protected:
  int width;
  int height;
};

// Derived classes
class Rectangle: public Shape {
  public:
  int getArea(char * me) {
    return (width * height);
  }
};

产生以下 LLVM IR:

%class.Rectangle = type { %class.Shape }
%class.Shape = type { i32 (...)**, i32, i32 }

这是什么“ i32 (...)** ”?它有什么作用?

从“i32 (...)**”的外观来看,这看起来像函数指针,但用于位转换对象。

像这样:

define linkonce_odr dso_local void @_ZN9RectangleC2Ev(%class.Rectangle* %0) unnamed_addr #5 comdat align 2 {
  %2 = alloca %class.Rectangle*, align 8
  store %class.Rectangle* %0, %class.Rectangle** %2, align 8
  %3 = load %class.Rectangle*, %class.Rectangle** %2, align 8
  %4 = bitcast %class.Rectangle* %3 to %class.Shape*
  call void @_ZN5ShapeC2Ev(%class.Shape* %4) #3
  %5 = bitcast %class.Rectangle* %3 to i32 (...)***
  store i32 (...)** bitcast (i8** getelementptr inbounds ({ [3 x i8*] }, { [3 x i8*] }* @_ZTV9Rectangle, i32 0, inrange i32 0, i32 2) to i32 (...)**), i32 (...)*** %5, align 8
  ret void
}

【问题讨论】:

  • i32 (...)* 是一个指向返回 i32 的可变参数函数的指针。在这里你有双指针。这可能应该理解为一个函数数组,即这看起来像虚拟调用的 vtable 实现。这也解释了可变参数(...) 参数,因为 vtable 涵盖了所有可能的函数签名。虽然我不确定它为什么返回i32。我认为@_ZN9RectangleC2Ev 是一个构造函数。该代码是否在实例上设置了适当的 vtable?我不确定,代码是一些奇怪的实现细节。
  • @freakish 你在很多层面上让我感到困惑。将虚函数的返回类型更改为任何东西,它仍然是“i32(...)**”,我从@_ZN9RectangleC2Ev 了解到的是,它创建了一个形状对象并将其存储到“i32(...)*** ”。指针是右指针。 i8* 应该可以完成这项工作,但它会将 Shape 对象存储到 i32 (...)** 中。

标签: c++ clang llvm clang++ llvm-ir


【解决方案1】:

让我们看更简单的代码

struct A {
  virtual void f1();
  int width;
};

struct B: public A {
  void f1() {};
};

B a;

编译后可以得到this:

%struct.B = type { %struct.A.base, [4 x i8] }
%struct.A.base = type <{ i32 (...)**, i32 }>

@a = dso_local local_unnamed_addr global %struct.B { %struct.A.base <{ i32 (...)** bitcast (i8** getelementptr inbounds ({ [3 x i8*] }, { [3 x i8*] }* @_ZTV1B, i32 0, inrange i32 0, i32 2) to i32 (...)**), i32 0 }>, [4 x i8] zeroinitializer }, align 8
@_ZTV1B = linkonce_odr dso_local unnamed_addr constant { [3 x i8*] } { [3 x i8*] [i8* null, i8* bitcast ({ i8*, i8*, i8* }* @_ZTI1B to i8*), i8* bitcast (void (%struct.B*)* @_ZN1B2f1Ev to i8*)] }, comdat, align 8

如您所见,i32 (...)** 是为_ZTV1B,在拆解后将变为vtable for B

我们看到这个神秘的功能是:

getelementptr inbounds ({ [3 x i8*] }, { [3 x i8*] }* @_ZTV1B, i32 0, inrange i32 0, i32 2)

GEP 之后是_ZN1B2f1Ev,解构之后是B::f1()

我也试过this example

auto f(B *a) {
    a->f1();
}

生成的代码是:

define dso_local void @_Z1fP1B(%struct.B* %0) local_unnamed_addr #0 {
  %2 = bitcast %struct.B* %0 to void (%struct.B*)***
  %3 = load void (%struct.B*)**, void (%struct.B*)*** %2, align 8, !tbaa !3
  %4 = load void (%struct.B*)*, void (%struct.B*)** %3, align 8
  tail call void %4(%struct.B* nonnull align 8 dereferenceable(12) %0)
  ret void
}

如您所见,它只是获取所需的函数并调用它。

P.S.

我们目前使用 i32 (...)** 作为 LLVM 中 vptr 字段的类型 结构类型。 LLVM 的 GlobalOpt 更喜欢将任何比特广播放在一边 存储的数据,而不是存储到的指针。

【讨论】:

  • @MusaKalaycı 标记为已解决,如果可能的话 :)
  • 我会尽快了解如何。让我打开另一个问题:)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-08-12
  • 2017-06-11
  • 2018-03-05
  • 2023-03-27
  • 1970-01-01
相关资源
最近更新 更多