LLVM-02 类型系统

本文系统介绍 LLVM IR 强类型、显式、静态的类型体系,结合 C 语言示例与 IR 代码,清晰说明各类类型的表示、用法与设计特点。

The LLVM IR type system is a low-level, language-independent framework that provides strict type information for all values and operations within the LLVM Intermediate Representation (IR). It is designed to be expressive enough to map high-level language constructs while remaining simple enough for efficient optimization and code generation.

LLVMIR类型系统是强类型、显式、静态的类型系统,所有值和表达式都有唯一、确定的类型,不允许隐式类型转换,类型不匹配会直接报错。

一级类型

所有指令只能产生一级类型的值,可存寄存器、作参数或者返回值。一级类型包括整数类型、浮点类型、指针类型、向量类型等。

整数类型

如下的C语言程序:

#include <stdio.h>

int main()
{
    short s = 1;
    int i = 10;
    long l = 100;
    long long ll = 1000;

    unsigned short us = 1;
    unsigned int ui = 10;
    unsigned long ul = 100;
    unsigned long long ull = 1000;
}

经过Clang翻译为IR:

...
define dso_local i32 @main() #0 {
  %1 = alloca i16, align 2
  %2 = alloca i32, align 4
  %3 = alloca i64, align 8
  %4 = alloca i64, align 8
  %5 = alloca i16, align 2
  %6 = alloca i32, align 4
  %7 = alloca i64, align 8
  %8 = alloca i64, align 8
  store i16 1, ptr %1, align 2
  store i32 10, ptr %2, align 4
  store i64 100, ptr %3, align 8
  store i64 1000, ptr %4, align 8
  store i16 1, ptr %5, align 2
  store i32 10, ptr %6, align 4
  store i64 100, ptr %7, align 8
  store i64 1000, ptr %8, align 8
  ret i32 0
}
...
  • alloca指令用于分配内存。

  • 整数类型采用iN表示,i16/i32/i64表示对应位数的整数,不仅支持标准位宽,也支持诸如i1(布尔值)、i7等任意位宽。

  • LLVM 的整数类型不区分有符号和无符号,有符号或无符号的语意由后续计算指令(如sdivvsudiv)来决定。

  • align表示按照一定的字节进行内存对齐。

  • 同样的,char类型对应为i8,即等同于 8 位整数。

浮点类型

同样将如下C语言程序翻译为IR:

#include <stdio.h>

int main()
{
    float f = 3.14;
    double d = 3.1415;
}
...
define dso_local i32 @main() #0 {
  %1 = alloca float, align 4
  %2 = alloca double, align 8
  store float 0x40091EB860000000, ptr %1, align 4
  store double 3.141500e+00, ptr %2, align 8
  ret i32 0
}
...
  • float代表 32 位单精度浮点数,double代表 64 位双精度浮点数。

  • IR在字面上会使用精确的十六进制来表示某些浮点常量,避免精度丢失(如0x40091EB860000000)。

指针类型

同样将如下C语言程序翻译为IR:

#include <stdio.h>

int main()
{
    int x = 10;
    int *p = &x;
    int val = *p;
}
...
define dso_local i32 @main() #0 {
  %1 = alloca i32, align 4
  %2 = alloca ptr, align 8
  %3 = alloca i32, align 4
  store i32 10, ptr %1, align 4
  store ptr %1, ptr %2, align 8
  %4 = load ptr, ptr %2, align 8
  %5 = load i32, ptr %4, align 4
  store i32 %5, ptr %3, align 4
  ret i32 0
}
...
  • 在 LLVM 15 之前,指针是带有具体类型的(例如i32*float*)。但自 LLVM 15 起,LLVM 全面弃用强类型指针,统一改用不透明指针ptr

  • ptr类型只代表这是一个内存地址,不关心它指向什么数据类型。当你使用loadstore时,必须显式指明你要读写的类型(如load i32, ptr %4明确说明“从指针%4处读取一个 i32 类型的值”)。

向量类型

同样将如下C语言程序翻译为IR:

#include <stdio.h>

int main()
{
    int vec[4] = {1,2,3,4};
}
...
define dso_local i32 @main() #0 {
  %1 = alloca [4 x i32], align 16
  call void @llvm.memcpy.p0.p0.i64(ptr align 16 %1, ptr align 16 @__const.main.vec, i64 16, i1 false)
  ret i32 0
}
...
  • <4 x i32>才是严格意义上的SIMD向量类型(用于硬件加速),上述IR中的[4 x i32]实际上是数组类型(属于聚合类型)

  • llvm.memcpy是内置函数,用于在内存之间直接进行大块数据的拷贝(在此处用来完成数组初始化)。

标签类型

同样将如下C语言程序翻译为IR:

#include <stdio.h>

int main()
{
entry:
    goto exit;

exit:
    return 0;
}
...
define dso_local i32 @main() #0 {
  %1 = alloca i32, align 4
  store i32 0, ptr %1, align 4
  br label %2

2:                                                ; preds = %0
  br label %3

3:                                                ; preds = %2
  ret i32 0
}
...
  • 标签类型用于指代基本块的起始位置

  • 标签类型不能存储在内存中,不能放入常规虚拟寄存器,它专门供跳转指令(如brswitch)或phi节点使用,作为控制流的直接目标。

元数据类型

除了计算数据,IR 中经常还会挂载元数据,用于辅助优化器或存储调试信息。元数据在IR中以!开头,它们不是程序实际运行的值,即使删除它们,程序的逻辑语义也完全不变,例如!dbg !10用于指定调试时的行号信息,!tbaa用于提供基于类型的别名分析信息以辅助优化。

聚合类型

聚合类型可以将多个类型组合在一起,最常见的就是数组和结构体。与只能放在寄存器里的一级类型不同,聚合类型通常存储在内存中,通过指针和专用的指针计算指令进行访问。

将如下包含结构体定义和成员访问的C语言程序翻译为IR:

#include <stdio.h>

struct Point {
    int x;
    double y;
};

int main()
{
    struct Point p;
    p.x = 10;
    p.y = 3.14;
}
...
; 结构体类型定义
%struct.Point = type { i32, double }

define dso_local i32 @main() #0 {
  %1 = alloca %struct.Point, align 8

  ; 获取结构体成员 x 的指针(索引为 0)
  %2 = getelementptr inbounds %struct.Point, ptr %1, i32 0, i32 0
  store i32 10, ptr %2, align 8

  ; 获取结构体成员 y 的指针(索引为 1)
  %3 = getelementptr inbounds %struct.Point, ptr %1, i32 0, i32 1
  store double 3.140000e+00, ptr %3, align 8

  ret i32 0
}
...

%struct.Point = type { i32, double }:在全局定义了一个自定义的结构体类型,包含一个i32和一个double

getelementptr是 LLVMIR中用于计算聚合类型内存地址的核心指令。它不进行真正的内存读取,仅仅根据基地址和索引(例如i32 0表示解引用结构体本身指针,后续的i32 0i32 1表示获取第几个成员)精确计算出目标成员的指针地址。

函数类型

函数类型描述了函数的签名,包括返回值类型和所有参数的类型。

将如下C语言程序翻译为IR:

#include <stdio.h>

int add(int a, int b) {
    return a + b;
}

int main() {
    int res = add(1, 2);
}
...
; add 函数的定义,其函数类型签名为:i32 (i32, i32)
define dso_local i32 @add(i32 noundef %0, i32 noundef %1) #0 {
  %3 = alloca i32, align 4
  %4 = alloca i32, align 4
  store i32 %0, ptr %3, align 4
  store i32 %1, ptr %4, align 4
  %5 = load i32, ptr %3, align 4
  %6 = load i32, ptr %4, align 4
  %7 = add nsw i32 %5, %6
  ret i32 %7
}

define dso_local i32 @main() #0 {
  %1 = alloca i32, align 4
  ; 调用 add 函数,必须严格遵守 i32 (i32, i32) 的类型匹配
  %2 = call i32 @add(i32 noundef 1, i32 noundef 2)
  store i32 %2, ptr %1, align 4
  ret i32 0
}
...

在IR中,函数本身被视为一种类型。例如@add的类型实际上是指向i32 (i32, i32)这种函数类型的指针。call指令在执行时,不仅需要提供目标函数指针,还必须在语句中显式写明完整的函数签名和参数类型,这是强类型系统的直接体现。

空类型

空类型代表“没有值”,它主要用于表示不返回任何结果的函数。由于它没有任何大小,因此不能分配内存,不能放入寄存器,也不能作为运算指令的操作数

void doNothing() { return; }

int main() { doNothing(); }
define dso_local void @doNothing() #0 {
  ret void  ; 专门用于返回空类型的终结指令
}

define dso_local i32 @main() #0 {
  ; 调用 void 函数,无需且无法使用虚拟寄存器接收返回值
  call void @doNothing()
  ret i32 0
}

显式类型转换

由于 LLVMIR绝对不允许隐式类型转换,前端必须使用明确的转换指令来打通不同类型。这也是 LLVM 类型系统“严谨性”的直接体现。

int main() {
    int a = 10;
    short b = (short)a;   // 整型截断
    float c = (float)a;   // 整型转浮点
}
define dso_local i32 @main() #0 {
  %1 = alloca i32, align 4
  %2 = alloca i16, align 2
  %3 = alloca float, align 4

  store i32 10, ptr %1, align 4
  %4 = load i32, ptr %1, align 4

  ; int (i32) 转 short (i16) -> 使用 trunc (Truncate) 指令
  %5 = trunc i32 %4 to i16
  store i16 %5, ptr %2, align 2

  ; int (i32) 转 float -> 使用 sitofp (Signed Integer To Floating Point) 指令
  %6 = load i32, ptr %1, align 4
  %7 = sitofp i32 %6 to float
  store float %7, ptr %3, align 4

  ret i32 0
}

常用的转换指令包含:trunc(大截小)、zext(零扩展,小展大)、sext(符号扩展)、sitofp/fptosi(整型和浮点互转)、bitcast(按位转换)。

LICENSED UNDER CC BY-NC-SA 4.0
评论