LLVM IR中函数声明用declare、定义用define,二者不可混用且declare须在define前;函数名以@开头,类型显式声明,定义需entry块和SSA合规,末尾必有terminator指令。

declare 和 define 是函数声明与定义的唯二关键字
LLVM IR 中没有“函数原型”或“头文件”的概念,函数可见性完全由 declare 与 define 的使用决定。前者只告诉模块“这个函数存在,签名是这样”,后者则提供完整实现。两者不能混用:对同一个函数名重复 declare 是允许的(等价于 C 中多次声明),但若已 define,再 declare 就会触发解析器报错 redefinition of function。
-
declare必须出现在define之前(按文本顺序),否则某些旧版工具链(如 LLVM 12 及更早)可能在链接阶段报undefined reference - 函数名必须以
@开头,例如@printf、@my_helper;不能用数字或特殊符号开头,也不能含空格 - 返回类型和参数类型必须显式写出,不支持类型推导;
i32、ptr、void是最常见类型,ptr等价于i8*,但语义更清晰
函数定义必须包含 entry 基本块且满足 SSA 要求
define 后的函数体不是自由语句序列,而是由一个或多个基本块(basic block)组成,首个块必须叫 entry(名字可任意,但 Clang/llc 默认生成此名,且多数工具链依赖该约定)。每个基本块以标签结尾(如 entry:),内部指令必须遵守静态单赋值(SSA)规则:每个寄存器变量(以 % 开头)只能被赋值一次。
- 局部变量若需多次写入,必须用
alloca分配栈空间,再配合store/load操作,例如:%x = alloca i32→store i32 42, i32* %x→%y = load i32, i32* %x - 直接写
%tmp = add i32 %a, %b这类指令产生的%tmp是 SSA 值,不可再次赋值;若需“更新”,必须新建变量名,如%tmp2 = mul i32 %tmp, 2 - 函数末尾必须有终止指令(terminator),如
ret、br、unreachable;漏写ret会导致验证失败:error: expected instruction terminator
参数传递和调用约定靠显式类型与属性控制
LLVM IR 不隐含 ABI 细节,函数参数是否通过寄存器传、是否压栈、是否被优化掉,取决于后端目标(triple)和函数属性(attributes)。但 IR 层必须明确声明参数类型,并可选附加调用约定(calling convention)或属性标记。
- 参数列表写在括号内,形如
(i32 %a, ptr %s);无参函数写(),不能省略括号 - 常见属性如
nounwind(不抛异常)、noalias(指针参数不别名)、nocapture(不存储指针副本)会影响优化,但不影响语法合法性 - 调用约定用
cc关键字指定,如define void @f() cc 11 { ... }表示使用fastcc;默认是c(C ABI),多数场景无需显式写 - 函数调用必须匹配签名:参数个数、类型、顺序全等;
call i32 @foo(i32 1)调用define i32 @foo(ptr %p)会报call argument mismatch
外部函数声明要小心类型一致性
对 libc 或系统函数做 declare 时,类型必须与实际 ABI 严格一致,尤其指针和整型宽度。Clang 生成的 declare 通常可靠,但手写时容易出错——比如把 @printf 声明为 i32 (i8*) 就漏了可变参数部分,正确应为 i32 (i8*, ...)。
-
...表示可变参数,仅用于declare,不能出现在define中;可变参数函数的 IR 实现依赖@llvm.va_start等 intrinsic - 字符串字面量要用
getelementptr转成指针:不能直接call @printf(i8* @.str),而要先%str_ptr = getelementptr [14 x i8], [14 x i8]* @.str, i64 0, i64 0再传入 - 全局常量字符串必须带
unnamed_addr和constant,否则链接器可能拒绝合并相同字符串,导致冗余
函数定义的骨架看似简单,但类型精度、SSA 约束、终止指令缺失、参数传递细节这四点,是手写或生成 IR 时最常卡住的地方。特别是 alloca 和 load/store 的配对,以及 getelementptr 的索引写法,稍有不慎就过不了 llvm-as 验证。

















