LLVMのalloca命令にハマった件(ただし単にallocaへの理解が間違っていただけな模様。)

LLVMのalloca命令はC言語のalloca関数と同様、Cのスタック上にメモリを確保するための命令です。単に一時的なメモリ領域を確保するのには便利ですが、ループの中で使うと何故かSegmentation faultを起こす時があり、LLVMのバグなのかと疑った時もありました。

以下がallocaを呼び出しつづけるとSegmentation Faultが発生する再現コードとなります。

; ModuleID = 'alloca.cpp'
%struct.x = type { i32 }

define void @f(%struct.x* nocapture %x) nounwind uwtable noinline ssp {
	ret void
}

define i32 @main(i32 %argc, i8** nocapture %argv) nounwind uwtable ssp {
	br label %1
	
	; <label>:1                                       ; preds = %1, %0
	%i.01 = phi i32 [ 0, %0 ], [ %2, %1 ]
	%x = alloca %struct.x, align 4
	call void @f(%struct.x* %x)
	%2 = add nsw i32 %i.01, 1
	%exitcond = icmp eq i32 %2, 6000000
	br i1 %exitcond, label %3, label %1
	
	; <label>:3                                       ; preds = %1
	ret i32 0
}

たしかにSegmentation Faultを起こしています。

$ gdb -arg lli alloca.ll 
(gdb) r
Starting program: /usr/local/bin/lli alloca.ll
Reading symbols for shared libraries ++........................ done

Program received signal EXC_BAD_ACCESS, Could not access memory.
Reason: KERN_PROTECTION_FAILURE at address: 0x00007fff5f3ffff8
0x000000010232502e in ?? ()
(gdb) bt
#0  0x000000010232502e in ?? ()
#1  0x000000010000ecb4 in llvm::JIT::runFunction (this=0x102402fd0, F=0x102401c00, ArgValues=@0x7fff5fbff0a0) at /Users/masa/src/llvm/lib/ExecutionEngine/JIT/JIT.cpp:428
#2  0x00000001001a0954 in llvm::ExecutionEngine::runFunctionAsMain (this=0x102402fd0, Fn=0x102401c00, argv=@0x100d37318, envp=0x7fff5fbff4b8) at /Users/masa/src/llvm/lib/ExecutionEngine/ExecutionEngine.cpp:398
#3  0x00000001000029c5 in main (argc=2, argv=0x7fff5fbff4a0, envp=0x7fff5fbff4b8) at /Users/masa/src/llvm/tools/lli/lli.cpp:286
(gdb) 

ここで、一旦落ち着いてLLVMのallocaの問題なのかを切り分けるためにC言語で同じコードを書いても再現するかを見てみましょう。

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char const* argv[])
{
    while(1) {
        void *a = alloca(1024);
        fprintf(stderr, "%p\n", a);
    }
    return 0;
}

//$ gcc -O2 alloca.c && ./a.out
//0x7fff5f405a10
//(snip)
//0x7fff5f402540
//0x7fff5f402130
//0x7fff5f401d20
//0x7fff5f401910
//0x7fff5f401500
//Segmentation fault

この場合でもやはりSegmentation Faultが発生しています。そもそもalloca()はどのような挙動をするのでしょうか?LLVMのalloca命令の説明をみてみると

The ‘alloca‘ instruction allocates memory on the stack frame of the currently
executing function, to be automatically released when this function returns to its caller.
(引用元: http://llvm.org/docs/LangRef.html#alloca-instruction )


alloca命令はスタックフレームにメモリを割り当てる。割り当てたメモリは呼び出し元の関数をreturnするときに自動的に開放されるとあります。また、linuxのmanページにもalloca()について同様の記述が見られます。

The alloca() function allocates size bytes of space in the stack frame of
the caller. This temporary space is automatically freed when the function
that called alloca() returns to its caller.
(引用元: http://man7.org/linux/man-pages/man3/alloca.3.html )

allocaで確保されるメモリがループから抜けるときに自動的に開放されると勘違いしていたのが原因のようです。思い返してみれば、allocaを使った場合の注意点として必ず挙がるポイントの1つでしたね。

1年くらい前に書いて放置していたんだけど、Inside yarv2llvm(その5)を見たらLLVMのallocaについて同じ所でハマっていたようなので公開しておく。

LLVM MCJITを試してみた

先月LLVMの最新バージョン3.2がリリースされました。年末はLLVMJITコンパイラを作ろうと思っていたので、いい機会と思いコード生成を中心にコードやドキュメントを眺めていました。
今回はJIT機構の1つであるMCJITでJITをする方法を調査しました。(MCJITのMCはMachine Codeの略となっています。MachineCodeについてはIntro to the LLVM MC Projectが詳しいです。)

MCJITはいままでのJIT機構(便宜上OldJITと呼びます)と同じようにLLVM IRで表現された関数をネイティブコードに変換する機構を持っています。
MCJITはMLを読む限り、いくつかの機能(lazy compilationなど)がサポートされていないため、すぐにOldJITを置き換えるものになるわけではないようです。現にLLVMJITフレームワークとして利用しているプロジェクトではこれまでどおりOldJITが利用されている模様。

MCJITはOldJITと同様にEngineBuilderでExecutionEngineを作成する形を取ります。
Engineの生成の際にUseMCJITのフラグを立てることでMCJITが利用できます。

InitializeNativeTarget();
InitializeNativeTargetAsmPrinter();
Module *Mod = new Module("LLVM", getGlobalContext());
Mod->setTargetTriple(LLVM_HOSTTRIPLE);
std::string Error;
JITMemoryManager *JMM = JITMemoryManager::CreateDefaultMemManager();
ExecutionEngine *EE = EngineBuilder(Mod)
    .setEngineKind(EngineKind::JIT)
    .setUseMCJIT(true)
    .setJITMemoryManager(JMM)
    .setErrorStr(&Error).create();

これでおしまいで良かったのですが、いくつか詰まった点があったので列挙しておきます。

  1. JITMemoryManagerの設定

OldJITではEngineBuilderにJITMemoryManagerを渡さなくてもExecutionEngineではデフォルトのMemoryManagerを内部で利用していました。MCJITでは該当するコードがなかったため、EngineBuilderにJITMemoryManagerの設定が必要でした。

  1. 外部定義の関数の利用

外部定義の関数のアドレス解決にMCJITではJITMemoryManagerとMCJIT用DynamicLinker(RuntimeDyld)からシンボルを検索します。

RuntimeDyldではExecutionEngineとは別のシンボルテーブルでアドレスを管理しており、ExecutionEngineでマッピングした関数のアドレスが検索できない場合がありました。 

上記コードではMemoryManagerにDefaultJITMemoryManager(以下DefaultJMM)を利用しておりDefaultJMMではllvm::sys::DynamicLibraryクラスで開いたライブラリからアドレスを検索します。
今回はsys::DynamicLibraryに利用するライブラリを逐次追加していく形を取りました。(下記コードでは自分自身のみを登録)

if(!sys::DynamicLibrary::LoadLibraryPermanently(NULL, &Error)) {
  std::cout << Error << std::endl;
}  
  1. AsmPrinterの設定

これもOldJITと微妙に挙動が違う点ですがAsmPrinterの初期化が必要でした。InitializeNativeTargetAsmPrinter()を呼ぶ必要がありました。

MCJITはllvm3.2では、一旦MCJITがコードをコンパイルすると、あとから新たに関数を追加してコードをコンパイルすることができません。


当初作りたかったJITコンパイラではコードの再コンパイル機能が必須だったのでOldJITを使う予定です。

BasickBlockに後から命令を追加する

最初にIRBuilderで作成したBasicBlockに後からInstructionを
追加する必要が出てきたので、コードを書いた。

BasickBlockは必ずBranchInstかReturnInstで終わっているはず
なので、その直前に新たに命令をinsertしていく。

#include <llvm/LLVMContext.h>
#include <llvm/Module.h>
#include <llvm/Support/IRBuilder.h>
using namespace llvm;

Function *createFunc(Module *m) {
    /* void test() { return; } */
    Type *voidTy = Type::getVoidTy(m->getContext());
    std::vector<Type*> args;
    FunctionType *Ty = FunctionType::get(voidTy, args, false);
    Function *F = Function::Create(Ty, GlobalValue::ExternalLinkage, "test", m);
    BasicBlock *bb = BasicBlock::Create(m->getContext(), "EntryBlock", F);
    IRBuilder<> *builder = new IRBuilder<>(bb);
    builder->CreateRetVoid();
    return F;
}

int main(int argc, char **argv)
{
    LLVMContext &Context = getGlobalContext();
    Type   *intTy  = Type::getInt32Ty(Context);
    Module   *m    = new Module("test", Context);
    Function *F    = createFunc(m);
    Constant *Zero = ConstantInt::get(intTy, 0);
    BasicBlock *BB = F->begin();
    Instruction *I = --(BB->end());

    Type *types[]  = {intTy};
    Value *Idxs_[] = {Zero, Zero};

    std::vector<Type*>  fields(types, types+1);
    std::vector<Value*> Idxs(Idxs_, Idxs_+2);
    StructType* structTy = StructType::create(fields, "struct.x", false);

    AllocaInst *newinst    = new AllocaInst(structTy);
    GetElementPtrInst *gep = GetElementPtrInst::CreateInBounds(newinst, Idxs);
    StoreInst *store       = new StoreInst(ConstantInt::get(intTy, 10), gep);

    BB->getInstList().insert(I, newinst);
    BB->getInstList().insert(I, gep);
    BB->getInstList().insert(I, store);
    (*m).dump();
    return 0;
}

createFunc関数では以下の関数を生成しているが、無事alloca, gep, storeの命令が
挿入されていることが確認できた。

void test() { return; }
$ g++ a.cpp `llvm-config --cxxflags --libs core` && ./a.out
; ModuleID = 'test'

%struct.x = type { i32 }

define void @test() {
EntryBlock:
  %0 = alloca %struct.x
  %1 = getelementptr inbounds %struct.x* %0, i32 0, i32 0
  store i32 10, i32* %1
  ret void
}

自身のBitCodeを出力する

clangでコンパイルしたバイナリのBitcodeを出力する.
BitCodeをParseBitcodeFile関数を使ってロードし、
Moduleを作る。Module::dumpでdumpする。

clang -emit-llvmでbitcodeを生成する際にはlink時にシステムのリンカ(ld)
ではなくてllvm-ldを指定しないとうまくコンパイル出来なかった。

/*
 * clang dump.cpp -emit-llvm `llvm-config --cxxflags` -c -o dump.bc
 * llvm-ld dump.bc -o dump
 * ./dump
 */
#include <llvm/LLVMContext.h>
#include <llvm/Module.h>
#include <llvm/Bitcode/ReaderWriter.h>
#include <llvm/Support/MemoryBuffer.h>
#include <llvm/Support/system_error.h>
#include <llvm/ADT/OwningPtr.h>
#include <iostream>
using namespace llvm;

int main(int argc, char **argv)
{
    LLVMContext &Context = getGlobalContext();
    std::string ErrMsg;
    OwningPtr<MemoryBuffer> Buffer;
    std::string fname(argv[0]);
    if (error_code ec = MemoryBuffer::getFile(fname+".bc", Buffer)) {
        std::cout << "Could not open file" << ec.message() << std::endl;
    }
    Module *m = ParseBitcodeFile(Buffer.get(), Context, &ErrMsg);
    if (!m) {
        std::cout << "error" << ErrMsg << std::endl;
        return 1;
    }
    (*m).dump();
    return 0;
}

LLVM APIで構造体を扱う(LinkList)

linklistを扱う方法がわかったのでメモしておく。
1. StructType::create(Context, "struct.list")で空の構造体を構築
2. structTy->setBody(fields, false)で構造体の本体を設定
struct x { int v; struct x *next; };みたいなリストを構築できる。

#include <llvm/LLVMContext.h>
#include <llvm/Module.h>
#include <llvm/ExecutionEngine/JIT.h>
#include <llvm/Support/IRBuilder.h>
#include <llvm/Support/TargetSelect.h>
//  g++ list.cpp `llvm-config --cxxflags --libs` -Wall
#include <iostream>
using namespace llvm;

struct x { int v; struct x *next; };

int main(int argc, char **argv)
{
    InitializeNativeTarget();
    LLVMContext &Context = getGlobalContext();
    Module *m = new Module("test", Context);
    Type *intTy  = Type::getInt64Ty(Context);

    StructType* structTy = StructType::create(Context, "struct.list");
    std::vector<Type*> fields;
    fields.push_back(intTy);
    fields.push_back(PointerType::get(structTy, 0));
    if (structTy->isOpaque()) {
        structTy->setBody(fields, false);
    }

    /*
     * int f1(struct x *p) { return p->next->v; }
     */
    std::vector<Type*> args_type;
    args_type.push_back(PointerType::get(structTy, 0));

    FunctionType *fnTy = FunctionType::get(intTy, args_type, false);
    Function *func = Function::Create(fnTy,
            GlobalValue::ExternalLinkage, "f1", m);
    Value *v = func->arg_begin();
    BasicBlock *bb = BasicBlock::Create(Context, "EntryBlock", func);
    IRBuilder<> *builder = new IRBuilder<>(bb);
    v = builder->CreateStructGEP(v, 1);
    v = builder->CreateLoad(v, "load0");
    v = builder->CreateStructGEP(v, 0);
    v = builder->CreateLoad(v, "load1");
    builder->CreateRet(v);

    (*m).dump();
    {
        ExecutionEngine *ee = EngineBuilder(m). 
            setEngineKind(EngineKind::JIT).create();
        void *f = ee->getPointerToFunction(func);
        typedef int (*func_t) (struct x *);
        struct x o = {10, NULL}, v = {};
        v.next = &o;
        std::cout << ((func_t)f)(&v) << std::endl;
    }
    return 0;
}

関数のInline化

定数伝搬の最適化(IPSCCP)とかinline化パスを適応するとinline化できる。
最後にGlobalDeadCodeElimを行うと不要な関数も削除できる。

#include <llvm/LLVMContext.h>
#include <llvm/Module.h>
#include <llvm/Support/IRBuilder.h>
#include <llvm/Support/TargetSelect.h>
#include <llvm/Pass.h>
#include <llvm/PassManager.h>
#include <llvm/ExecutionEngine/JIT.h>
#include <llvm/ExecutionEngine/Interpreter.h>
#include <llvm/ExecutionEngine/ExecutionEngine.h>
#include <llvm/Target/TargetData.h>
#include <llvm/Transforms/Scalar.h>
#include <llvm/Transforms/IPO.h>
#include <iostream>
using namespace llvm;

Function *createAbs(Module *m, LLVMContext &Context, Type *floatTy) {
    std::vector<Type*>args_type;
    args_type.push_back(floatTy);
    FunctionType *tyFunc = FunctionType::get(floatTy, args_type, false);
    Function *func = Function::Create(tyFunc,
            GlobalValue::InternalLinkage, "fabs", m);
    BasicBlock *bb = BasicBlock::Create(Context, "EntryBlock", func);
    IRBuilder<> *builder = new IRBuilder<>(bb);
    Function::arg_iterator args = func->arg_begin();
    Value *arg0 = args++;arg0->setName("arg0");
    /*
     * float t1(float p) { return (p>0)?p:-p; }
     */
    Value *v    = builder->CreateFCmpOGE(arg0, ConstantFP::get(floatTy, 0.0));
    Value *v0   = builder->CreateFNeg(arg0);
    Value *ret  = builder->CreateSelect(v, arg0, v0);
    builder->CreateRet(ret);
    return func;
}

int main(int argc, char **argv)
{
    InitializeNativeTarget();
    LLVMContext &Context = getGlobalContext();
    Module *m = new Module("test", Context);
    Type *floatTy = Type::getFloatTy(Context);
    Function *f = createAbs(m, Context, floatTy);

    std::vector<Type*>args_type;
    args_type.push_back(floatTy);
    FunctionType *tyFunc = FunctionType::get(floatTy, args_type, false);
    Function *func = Function::Create(tyFunc, GlobalValue::ExternalLinkage, "test", m);
    BasicBlock *bb = BasicBlock::Create(Context, "EntryBlock", func);
    IRBuilder<> *builder = new IRBuilder<>(bb);
    Value *v = ConstantFP::get(floatTy, -10.0);
    v = builder->CreateCall(f, v);
    builder->CreateRet(v);
    std::cout << "before" << std::endl;
    (*m).dump();
    ExecutionEngine *ee = EngineBuilder(m).setEngineKind(EngineKind::JIT).create();
    PassManager mpm;
    mpm.add(createIPSCCPPass());
    mpm.add(createFunctionInliningPass());
    mpm.add(createLICMPass());
    mpm.add(createGVNPass());
    mpm.add(createGlobalDCEPass());
    mpm.run(*m);
    std::cout << std::endl << "before" << std::endl;
    (*m).dump();

    void *ptr = ee->getPointerToFunction(func);
    std::cout << ((float (*)(float))ptr)(-10.0) << std::endl;
    std::cout << fabs(-10.0) << std::endl;
    return 0;
}