Clang -编译一个C头到LLVM IR/位码

Clang - Compiling a C header to LLVM IR/bitcode

本文关键字:头到 LLVM IR 位码 一个 编译 Clang      更新时间:2023-10-16

假设我有以下简单的C头文件:

// foo1.h
typedef int foo;
typedef struct {
  foo a;
  char const* b;
} bar;
bar baz(foo*, bar*, ...);

我的目标是获取这个文件,并生成一个LLVM模块,看起来像这样:

%struct.bar = type { i32, i8* }
declare { i32, i8* } @baz(i32*, %struct.bar*, ...)

换句话说,将带有声明的C .h文件转换为等效的LLVM IR,包括类型解析、宏展开等。

通过Clang传递它来生成LLVM IR会产生一个空模块(因为实际上没有使用任何定义):

$ clang -cc1 -S -emit-llvm foo1.h -o - 
; ModuleID = 'foo1.h'
target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-darwin13.3.0"
!llvm.ident = !{!0}
!0 = metadata !{metadata !"clang version 3.5 (trunk 200156) (llvm/trunk 200155)"}

我的第一反应是转向谷歌,我遇到了两个相关的问题:一个来自邮件列表,一个来自StackOverflow。两者都建议使用-femit-all-decls标志,所以我尝试了:

$ clang -cc1 -femit-all-decls -S -emit-llvm foo1.h -o -
; ModuleID = 'foo1.h'
target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-darwin13.3.0"
!llvm.ident = !{!0}
!0 = metadata !{metadata !"clang version 3.5 (trunk 200156) (llvm/trunk 200155)"}

相同的结果。

我还尝试禁用优化(包括-O0-disable-llvm-optzns),但这对输出没有影响。使用以下变量是否产生所需的IR:

// foo2.h
typedef int foo;
typedef struct {
  foo a;
  char const* b;
} bar;
bar baz(foo*, bar*, ...);
void doThings() {
  foo a = 0;
  bar myBar;
  baz(&a, &myBar);
}

然后运行:

$ clang -cc1 -S -emit-llvm foo2.h -o -
; ModuleID = 'foo2.h'
target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-darwin13.3.0"
%struct.bar = type { i32, i8* }
; Function Attrs: nounwind
define void @doThings() #0 {
entry:
  %a = alloca i32, align 4
  %myBar = alloca %struct.bar, align 8
  %coerce = alloca %struct.bar, align 8
  store i32 0, i32* %a, align 4
  %call = call { i32, i8* } (i32*, %struct.bar*, ...)* @baz(i32* %a, %struct.bar* %myBar)
  %0 = bitcast %struct.bar* %coerce to { i32, i8* }*
  %1 = getelementptr { i32, i8* }* %0, i32 0, i32 0
  %2 = extractvalue { i32, i8* } %call, 0
  store i32 %2, i32* %1, align 1
  %3 = getelementptr { i32, i8* }* %0, i32 0, i32 1
  %4 = extractvalue { i32, i8* } %call, 1
  store i8* %4, i8** %3, align 1
  ret void
}
declare { i32, i8* } @baz(i32*, %struct.bar*, ...) #1
attributes #0 = { nounwind "less-precise-fpmad"="false" "no-frame-pointer-elim"="false" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-realign-stack" "stack-protector-buffer-size"="8" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #1 = { "less-precise-fpmad"="false" "no-frame-pointer-elim"="false" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-realign-stack" "stack-protector-buffer-size"="8" "unsafe-fp-math"="false" "use-soft-float"="false" }
!llvm.ident = !{!0}
!0 = metadata !{metadata !"clang version 3.5 (trunk 200156) (llvm/trunk 200155)"}

除了占位符doThings之外,这正是我想要的输出!问题是,这需要1.)使用修改过的标题版本,以及2.)提前知道事物的类型。这让我想到……

为什么?

基本上,我正在构建一个使用LLVM生成代码的语言的实现。实现应该只通过指定C头文件和相关的库(没有手动声明)来支持C互操作,然后编译器将在链接时间之前使用它们来确保函数调用与其签名匹配。因此,我将问题缩小到两个可能的解决方案:

  1. 将头文件转换为LLVM IR/bitcode,然后可以获得每个函数的类型签名
  2. 使用libclang解析报头,然后从结果AST中查询类型(我的'最后手段',以防这个问题没有足够的答案)

TL;博士

我需要取一个C头文件(如上面的foo1.h),并且在不改变它的情况下,使用Clang生成上述预期的LLVM IR,或者,找到另一种方法从C头文件(最好使用libclang或构建C解析器)获得函数签名

可能是不太优雅的解决方案,但仍然使用doThings函数的想法,该函数强制编译器发出IR,因为使用了定义:

这种方法有两个问题,一是需要修改头文件,二是需要对所涉及的类型有更深入的理解,以便生成要放入函数的"用途"。这两个问题都可以相对简单地解决:

  1. 不是直接编译头文件,而是从包含所有"uses"代码的.c文件中#include它(或者更有可能,它的预处理版本,或多个头文件)。简单明了:

    // foo.c
    #include "foo.h"
    void doThings(void) {
        ...
    }
    
  2. 您不需要详细的类型信息来生成名称的特定用法,将结构体实例化与参数相匹配,以及像上面的"uses"代码那样的所有复杂性。您实际上不需要自己收集函数签名

    您所需要的只是名称本身的列表,并跟踪它们是用于函数还是用于对象类型。然后你可以重新定义你的"uses"函数,像这样:

    void * doThings(void) {
        typedef void * (*vfun)(void);
        typedef union v { void * o; vfun f; } v;
        return (v[]) {
            (v){ .o = &(bar){0} },
            (v){ .f = (vfun)baz },
        };
    }
    

    这极大地简化了名称的必要"使用",要么将其强制转换为统一的函数类型(并获取其指针而不是调用它),要么将其包装在&(){0}中(无论它是什么都将其实例化)。这意味着您根本不需要存储实际的类型信息,只需要存储从标题中提取名称的上下文的类型。

    (显然要给虚拟函数和占位符类型扩展唯一名称,这样它们就不会与您实际想要保留的代码冲突)

这极大地简化了解析步骤,因为您只需要识别结构/联合或函数声明的上下文,而实际上不需要对周围的信息做太多的处理。


一个简单但有点粗俗的起点(我可能会使用它,因为我的标准很低:D)可能是:

  • grep通过头的#include指令,接受尖括号参数(即一个已安装的头,你不想生成声明)。
  • 使用此列表创建一个虚拟包含文件夹,其中包含所有必要的包含文件,但为空
  • 预处理它,希望能简化语法(clang -E -I local-dummy-includes/ -D"__attribute__(...)=" foo.h > temp/foo_pp.h或类似的东西)
  • grep通过structunion后面跟着一个名字,}后面跟着一个名字,或name (,并使用这个荒谬的简化非解析来构建虚拟函数中的用途列表,并发出。c文件的代码。

它不会捕捉到所有的可能性;但是,通过一些调整和扩展,它实际上可能会处理现实头代码的一个大子集。您可以在以后的阶段用专用的简化解析器(仅用于查看所需上下文的模式)替换它。