You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用LLVM C++ API操作结构体指针时函数查找段错误问题

LLVM JIT调用结构体指针函数时的段错误问题

我想为特定用途开发一门专用语言,本质是一组可被C代码调用的函数,需要传递C结构体指针。为验证自己对LLVM结构体指针操作的掌握程度,我写了一个给结构体int成员加2的程序,但在JIT查找函数时持续触发段错误,LLVM IR还没修正。

问题代码与环境

结构体定义

struct test_struct {
    char *b;
    int *c_dummy;
    int c;
    double f ;
};
struct test_struct tt{0,0,1,0};

LLVM初始化代码

InitializeNativeTarget();
InitializeNativeTargetAsmPrinter();
InitializeNativeTargetAsmParser();
TheJIT = ExitOnErr(KaleidoscopeJIT::Create());
TheContext = std::make_unique<LLVMContext>();
TheModule = std::make_unique<Module>("my cool jit", *TheContext);
TheModule->setDataLayout(TheJIT->getDataLayout());
Builder = std::make_unique<IRBuilder<>>(*TheContext);
TheFPM = std::make_unique<legacy::FunctionPassManager>(TheModule.get());
TheFPM->add(createCFGSimplificationPass());
TheFPM->doInitialization();

// create the value 2 to add later.
Value *v_const = ConstantInt::get(*TheContext,APInt(32,2,true));
// The return type of the function:
Type *r_type = Type::getVoidTy(*TheContext);

函数与IR构建代码

ArrayRef<Type *> params = {Type::getInt8PtrTy(*TheContext),Type::getInt32PtrTy(*TheContext),
    Type::getInt32Ty(*TheContext),Type::getDoubleTy(*TheContext)};
StructType *st = StructType::create(*TheContext,"s");
st->setBody(params);
/* Define function.*/
ArrayRef<Type *> params_func = ArrayRef<Type *>(st);
FunctionType *ft = FunctionType::get(r_type,params_func,false);
Function *F = Function::Create(ft,Function::ExternalLinkage,"test",*TheModule); // add a function to the module

Value *c;
BasicBlock *bb = BasicBlock::Create(*TheContext,"entry",F);
Builder->SetInsertPoint(bb);
Value *mem= Builder->CreateAlloca(st,nullptr); // This should allocate the param to stack.
Value *mem_int= Builder->CreateAlloca(Type::getInt32Ty(*TheContext),nullptr); 
// create a stac allocation for st.
for (auto arg = F->arg_begin();arg != F->arg_end();arg++)
{
    // bring st from the argument.
    Builder->CreateStore(arg,mem); // store rhe struct pointer in the stack.
    auto tmp_ar = arg;
    c = Builder->CreateStructGEP(st,mem,2); // take the second element from the pointer.
}
/* Store the argument in the stack*/
mem_int = Builder->CreateLoad(st,c);  
// perform add
Value *res = Builder->CreateAdd(mem_int,v_const,"add tmp"); 
// close of.
Builder->CreateStore(res, mem_int);
Builder->CreateRetVoid();
/// added a basic block and returned void.

TheModule->dump();
auto RT = TheJIT->getMainJITDylib().createResourceTracker();
auto TSM = ThreadSafeModule(std::move(TheModule),std::move(TheContext));
ExitOnErr(TheJIT->addModule(std::move(TSM),RT));


F->print(errs());
TheJIT->dumpEE();
do_test();

测试函数

void do_test()
{
    auto fsym = ExitOnErr(TheJIT->lookup("test"));
    void (*add_func)(struct test_struct *) = (void (*)(struct test_struct *))(intptr_t)fsym.getAddress();
    printf("Before func: %d\n", tt.c);
    add_func(&tt); // this should change t.c
    printf("After func: %d\n", tt.c);
}

编译命令

g++ -g test_ptr.cpp  `llvm-config --cxxflags --ldflags --system-libs --libs core orcjit native` -O0 -o test_ptr

错误原因分析

  1. 函数参数类型不匹配
    C端传递的是struct test_struct*,但LLVM中定义的函数参数是s类型(结构体值,而非指针),这导致调用约定完全不匹配,JIT加载后调用时栈结构混乱,直接触发段错误。

  2. 内存操作逻辑错误

    • 不必要的栈分配:代码中用CreateAlloca分配结构体栈空间,但实际上应该直接操作传入的结构体指针,无需额外拷贝。
    • Load类型错误:c是结构体成员c的指针(int*),但CreateLoad时传入的类型是结构体st,类型不匹配会导致IR非法,运行时崩溃。
  3. 悬空指针访问
    将TheModule和TheContext通过std::move转移给ThreadSafeModule后,原指针F(属于原模块)变为悬空指针,后续调用F->print(errs())会触发未定义行为,也是段错误的潜在诱因。

修正后的代码

修正结构体与函数定义

// 匹配C结构体的名称和布局,确保内存对齐一致
StructType *st = StructType::create(*TheContext, "test_struct");
st->setBody({
    Type::getInt8PtrTy(*TheContext),
    Type::getInt32PtrTy(*TheContext),
    Type::getInt32Ty(*TheContext),
    Type::getDoubleTy(*TheContext)
}, false); // 默认非packed,和C结构体对齐规则一致

// 函数参数改为结构体指针,匹配C端调用签名
ArrayRef<Type *> params_func = {st->getPointerTo()};
FunctionType *ft = FunctionType::get(r_type, params_func, false);
Function *F = Function::Create(ft, Function::ExternalLinkage, "test", *TheModule);

修正IR函数体

BasicBlock *bb = BasicBlock::Create(*TheContext, "entry", F);
Builder->SetInsertPoint(bb);

// 获取传入的结构体指针参数
Argument *structPtr = &*F->arg_begin();
structPtr->setName("struct_ptr");

// 直接通过指针访问第3个成员(索引从0开始,c对应索引2)
Value *cPtr = Builder->CreateStructGEP(st, structPtr, 2, "c_ptr");
// 加载成员当前值
Value *cVal = Builder->CreateLoad(Type::getInt32Ty(*TheContext), cPtr, "c_val");
// 执行加2操作
Value *newCVal = Builder->CreateAdd(cVal, v_const, "new_c_val");
// 将结果存回结构体成员
Builder->CreateStore(newCVal, cPtr);

Builder->CreateRetVoid();

修正模块处理逻辑

// 先打印模块内容,再转移模块
TheModule->dump();
F->print(errs());

auto RT = TheJIT->getMainJITDylib().createResourceTracker();
auto TSM = ThreadSafeModule(std::move(TheModule), std::move(TheContext));
ExitOnErr(TheJIT->addModule(std::move(TSM), RT));

TheJIT->dumpEE();
do_test();

关键修正点总结

  • 对齐LLVM结构体与C结构体的名称、成员类型和内存布局,确保调用时内存结构一致
  • 将LLVM函数参数改为结构体指针,完全匹配C端的函数签名
  • 直接操作传入的结构体指针,避免不必要的栈分配和拷贝
  • 修正Load/Store的类型匹配错误,保证IR合法性
  • 避免在模块转移后访问原模块的悬空指针

内容的提问来源于stack exchange,提问作者Igor de Paula

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 09:27:01