llvm ir笔记

一些概念摘要

  • LLVM IR 是强类型的

  • LLVM IR 不区分有符号整数和无符号整数。但是指令会区别有符号和无符号

  • LLVM IR 假定是二进制补码有符号整数,因此说截断同样适用于有符号和无符号整数。

  • 全局符号以@开头。

  • 局部符号以%开头。

  • 所有符号必须声明或定义。

  • 全局变量和栈上变量皆指针

站在前人的基础上

自己的一些理解

编译过程中无外乎涉及到的基本概念,符号,符号表,类型系统,数据布局使用。可以从这几个角度思考llvm ir的设计,加深自己的理解。

llvm ir的布局:

alt text

上述每个部分官方视频中都有详细介绍,推荐先看一遍

符号与符号表

  • 符号通常指程序中使用的变量、函数、类型以及其他标识符的名称。

  • 编译器通过符号表来管理这些符号,符号表是一种数据结构,用于存储程序中所有的符号及其相关信息。符号表中的每个条目都包含了一个符号的名称、类型、存储位置等信息。

  • LLVM中符号表在数据布局上分为三类:

    • 寄存器中的符号:%local_variable = add i32 1, 2 (局部变量赋值要符合SSA形式)

    • 栈上的符号:%local_variable = alloca i32

    • 数据区里的符号:指的是那些全局符号,如下:

int a;
extern int b;
static int c;
void d(void);
void e(void) {}
static void f(void) {}
@a = dso_local global i32 0, align 4
@b = external global i32, align 4
@c = internal global i32 0, align 4
declare void @d()
define dso_local void @e() {
  ret void
}
define internal void @f() {
  ret void
}
  • 链接类型:对于链接类型,我们常用的主要有什么都不加(默认为external)、private和internal。

  • 可见性:主要分为三种default, hidden和protected,这里主要的区别在于符号能否被重载。default的符号可以被重载,而protected的符号则不可以;此外,hidden则不将变量放在动态符号表中,因此其它的模块不可以直接引用这个符号。

  • 可抢占性:在我们日常看到的LLVM IR中,会经常见到dso_local这样的修饰符,在LLVM中被称作运行时抢占性修饰符。

类型系统

  • 常量

  • 各种基本类型、派生类型,官方详细介绍:https://llvm.org/docs/LangRef.html#type-system

  • structureunion:官方介绍https://llvm.org/docs/LangRef.html#structure-type,注意packed structure的区别

  • 函数类型:https://mapping-high-level-constructs-to-llvm-ir.readthedocs.io/en/latest/basic-constructs/functions.html

  • 元数据metadata

  • 类型转换指令:https://mapping-high-level-constructs-to-llvm-ir.readthedocs.io/en/latest/basic-constructs/structures.html

数据使用

c++特性的llvm ir实现

class和construct的实现

例子:https://compiler-explorer.com/z/e77T7sTP1

#include <stddef.h>

class Foo {
public:
    Foo() {
        _length = 0;
    }

    size_t GetLength() const {
        return _length;
    }

    void SetLength(size_t value) {
        _length = value;
    }

private:
    size_t _length;
};

int main() {
    Foo f;
    f.SetLength(1);
    return 0;
}

首先定义Foo的类型,只包括一个_length成员(如果类中有虚函数的话,还是首地址添加一个vtable*成员)

%class.Foo = type { i64 }

定义构造函数:注意llvm15之后使用的都是不透明指针ptr类型,而不是Foo*。可以看到这里会对成员变量初始化

define linkonce_odr dso_local void @Foo::Foo()(ptr noundef nonnull align 8 dereferenceable(8) %this) unnamed_addr comdat align 2 {
entry:
  %this.addr = alloca ptr, align 8
  store ptr %this, ptr %this.addr, align 8
  %this1 = load ptr, ptr %this.addr, align 8
  %_length = getelementptr inbounds %class.Foo, ptr %this1, i32 0, i32 0
  store i64 0, ptr %_length, align 8
  ret void
}

定义成员函数:

define linkonce_odr dso_local void @Foo::SetLength(unsigned long)(ptr noundef nonnull align 8 dereferenceable(8) %this, i64 noundef %value) comdat align 2 {
entry:
  %this.addr = alloca ptr, align 8
  %value.addr = alloca i64, align 8
  store ptr %this, ptr %this.addr, align 8
  store i64 %value, ptr %value.addr, align 8
  %this1 = load ptr, ptr %this.addr, align 8
  %0 = load i64, ptr %value.addr, align 8
  %_length = getelementptr inbounds %class.Foo, ptr %this1, i32 0, i32 0
  store i64 %0, ptr %_length, align 8
  ret void
}

main函数中使用:会alloca这个class的,并调用构造函数。

define dso_local noundef i32 @main() {
entry:
  %retval = alloca i32, align 4
  %f = alloca %class.Foo, align 8
  store i32 0, ptr %retval, align 4
  call void @Foo::Foo()(ptr noundef nonnull align 8 dereferenceable(8) %f)
  call void @Foo::SetLength(unsigned long)(ptr noundef nonnull align 8 dereferenceable(8) %f, i64 noundef 1)
  ret i32 0
}

vtable in llvm ir

下面例子介绍的是简单的单继承ir形式,多继承的ir更复杂,我专门在这篇详细介绍了

例子:https://compiler-explorer.com/z/afv6GPdPb

class A {
public:
  A() {a = 0;}
 virtual void foo() {};
 virtual void bar() {};
 int a;
 int b;
};
class B : public A {
public:
 B() {a = 1; b = 2; c = 3;}
 void bar() override {};
 int b;
 int c;
};

void func(A *a) {
 a->bar();
}

int main() {
    A *a = new B();
    func(a);
    return 0;
}

type:

%class.A = type { ptr, i32, i32 }
%class.B = type { %class.A, i32, i32 }

vtable:

  • 如果class上有虚函数,那么这个class都有一个vtable虚函数表

  • 可以看出vtable在ir上是一个指针数组的全局变量

  • 多重继承的子类中是一个多维指针数组的vtable, 如{ [4 x ptr], [3 x ptr] }(思考下为什么不采用平铺的数组方式?)并且该子类对象有多个虚表指针

  • 并且编译器已经帮我们把每个vtable中的相关虚函数都放好了,相同虚函数的位置在每个vtable的索引都是固定的。因为子类会覆盖。

@vtable for A = linkonce_odr dso_local unnamed_addr constant { [4 x ptr] } { [4 x ptr] [
	ptr null, 
	ptr @typeinfo for A,
	ptr @A::foo(), 
	ptr @A::bar()] }, comdat, align 8
@vtable for B = linkonce_odr dso_local unnamed_addr constant { [4 x ptr] } { [4 x ptr] [
	ptr null, 
	ptr @typeinfo for B, 
	ptr @A::foo(), 
	ptr @B::bar()] }, comdat, align 8
@vtable for __cxxabiv1::__si_class_type_info = external global ptr
@vtable for __cxxabiv1::__class_type_info = external global ptr

@typeinfo name for A = linkonce_odr dso_local constant [3 x i8] c"1A\00", comdat, align 1
@typeinfo name for B = linkonce_odr dso_local constant [3 x i8] c"1B\00", comdat, align 1

@typeinfo for A = linkonce_odr dso_local constant { ptr, ptr } { 
	ptr getelementptr inbounds (ptr, ptr @vtable for __cxxabiv1::__class_type_info, i64 2), 
	ptr @typeinfo name for A }, comdat, align 8
@typeinfo for B = linkonce_odr dso_local constant { ptr, ptr, ptr } { 
	ptr getelementptr inbounds (ptr, ptr @vtable for __cxxabiv1::__si_class_type_info, i64 2), 
	ptr @typeinfo name for B, 
	ptr @typeinfo for A }, comdat, align 8

typeinfo:

  • 首先是 type_info 方法的辅助类,是 __cxxabiv1 里的某个类。 对于启用了 RTTI 的类来说,

    • 所有的基础类(没有父类的类)都继承于_class_type_info,

    • 所有的基础类指针都继承自__pointer_type_info

    • 所有的单一继承类都继承自__si_class_type_info

    • 所有的多继承类都继承自__vmi_class_type_info

  • 然后是指向存储类型名字的指针,如果有继承关系,则最后是指向父类的 typeinfo 的记录。

B::B()构造函数初始化过程:

  • 首先调用构造函数A::A()初始化,

  • 如果存在虚函数的话,虚表指针被初始化指向虚表的第一个虚函数的位置。

    • 这里getelementptr 0 2指向@vtable for B第三个索引的位置,即第一个虚函数的位置

  • 虚表指针存放在class结构体的第一个参数,即this指针(class对象)的基址上(注意多重继承的子类对象会有多个虚指针)

    • 这里将@vtable for B虚函数的基址(vtable的第三个索引)赋值到this的基址,即%class.A = type { ptr, i32, i32 }的ptr

  • 最后对成员变量初始化{a = 1; b = 2; c = 3;},分别对应:

    • %class.A = type { ptr, i32, i32 }

    • %class.B = type { %class.A, i32, i32 } (这里可以看出如果基类和子类有相同的变量赋值时,如b=2,父类不可见,是对当前类变量的赋值)

    • %class.B = type { %class.A, i32, i32 }

define linkonce_odr dso_local void @B::B()(ptr noundef nonnull align 8 dereferenceable(24) %this) unnamed_addr comdat align 2 {
entry:
  %this.addr = alloca ptr, align 8
  store ptr %this, ptr %this.addr, align 8
  %this1 = load ptr, ptr %this.addr, align 8
  call void @A::A()(ptr noundef nonnull align 8 dereferenceable(16) %this1)
  store ptr getelementptr inbounds ({ [4 x ptr] }, ptr @vtable for B, i32 0, inrange i32 0, i32 2), ptr %this1, align 8
  %a = getelementptr inbounds %class.A, ptr %this1, i32 0, i32 1
  store i32 1, ptr %a, align 8
  %b = getelementptr inbounds %class.B, ptr %this1, i32 0, i32 1
  store i32 2, ptr %b, align 8
  %c = getelementptr inbounds %class.B, ptr %this1, i32 0, i32 2
  store i32 3, ptr %c, align 4
  ret void
} 

调用虚函数:

首先需要取到该class的vtable地址,就是this指针的基址位置(构造函数的时候初始化好的)

取到vtable的虚函数指针,相关偏移是固定,编译器可以提前计算到,比如这里call a->bar()的在vtable的索引固定都是1

define dso_local void @func(A*)(ptr noundef %a) {
entry:
  %a.addr = alloca ptr, align 8
  store ptr %a, ptr %a.addr, align 8
  %0 = load ptr, ptr %a.addr, align 8
  %vtable = load ptr, ptr %0, align 8
  %vfn = getelementptr inbounds ptr, ptr %vtable, i64 1
  %1 = load ptr, ptr %vfn, align 8
  call void %1(ptr noundef nonnull align 8 dereferenceable(16) %0)
  ret void
}

https://llvm.org/devmtg/2021-11/slides/2021-RelativeVTablesinC.pdf

getelementptr理解