c++asmjit

Retrieve ptr from function call asmjit


I am trying to generate a function call using AsmJit to which I pass an char*. This char* is in itself retrieved from another function call. I tried out this:

typedef
const char* getStr();

const char* getStrImpl()  {
    return "hello pie";
}

void use_str_impl(int id, const char* c_str) {
    // do stuff...
}

int main() {
    JitRuntime rt;
    CodeHolder code;
    code.init(rt.getCodeInfo());

    X86Compiler c(&code);

    auto jitted_func = c.addFunc(FuncSignature0<const char*>(code.getCodeInfo().getCdeclCallConv()));
    auto err = c.getLastError();

    auto call = c.call((uint64_t) fooFuncImpl, FuncSignature0<intptr_t>());
    X86Gpd res(call->getRet().getId());

    auto call2 = c.call((uint64_t) send_input, FuncSignature2<void, int, intptr_t>());
    err = !call2->setArg(0, Imm(42));
    err = !call2->setArg(1, res);

    c.ret();

    c.endFunc();
    err = c.finalize();

    if(err) return 0;

    size_t size = code.getCodeSize();
    VMemMgr vm;

    void* p = vm.alloc(size);
    if (!p) return 0;

    code.relocate(p);
    auto fun = (entrypoint*) p;
    fun();
}

It turns out this does not generate any instructions for the second parameter or second call to setArg. I also tried to use .newIntPtr and using move instructions to move the result of call into place. But this generated dec and add instructions which made no sense to me and my small experience with assembly. What is the correct way of doing this type of thing?

Btw I am using the AsmJit next branch.


Solution

  • I have done few corrections to your sample with some comments.


    Better Usage of JitRuntime:

    JitRuntime rt;
    size_t size = code.getCodeSize();
    VMemMgr vm;
    ....
    void* p = vm.alloc(size);
    if (!p) return 0;
    code.relocate(p);
    auto fun = (entrypoint*) p;
    

    You have used JitRuntime just to setup the parameters for CodeHolder, but then avoided it and allocated the memory for the function yourself. While that's a valid use case it's not what most people do. Using runtime's add() is sufficient in most cases.


    Invalid use of CCFuncCall::getRet():

    X86Gpd res(call->getRet().getId());
    

    The call node at this point doesn't have any return register assigned so it would return an invalid id. If you need to create a virtual register you always have to call compiler's newSomething(). AsmJit's compiler provides API to check for that case at runtime, if you are unsure:

    // Would print 0
    printf("%d", (int)c.isVirtRegValid(call->getRet().getId()));
    

    The solution is to create a new virtual register and ASSIGN it to the function's return value. Assigning return value requires an index (like assigning an argument), the reason is that some functions may return multiple values(like 64-bit value in 32-bit mode), using 0 as index is sufficient most of the time.

    X86Gp reg = c.newIntPtr("reg");
    call->setRet(0, reg);
    

    You can verify getRet() functionality:

    X86Gp reg = c.newIntPtr("reg");
    assert(call->getRet(0).isNone());
    call->setRet(0, reg);
    assert(call->getRet(0) == reg);
    

    Fully working example:

    #include <stdio.h>
    #include <asmjit/asmjit.h>
    
    const char* func_a() {
      printf("func_a(): Called\n");
      return "hello pie";
    }
    
    void func_b(int id, const char* c_str) {
      printf("func_b(%d, %s): Called\n", id, c_str);
    }
    
    int main() {
      using namespace asmjit;
    
      JitRuntime rt;
    
      CodeHolder code;
      code.init(rt.getCodeInfo());
    
      X86Compiler c(&code);
      X86Gp reg = c.newIntPtr("reg");
    
      // Compilation step...
      c.addFunc(FuncSignature0<void>(code.getCodeInfo().getCdeclCallConv()));
    
      auto call_a = c.call((uint64_t)func_a, FuncSignature0<intptr_t>());
      call_a->setRet(0, reg);
    
      auto call_b = c.call((uint64_t)func_b, FuncSignature2<void, int, intptr_t>());
      call_b->setArg(0, Imm(42));
      call_b->setArg(1, reg);
    
      c.ret();
      c.endFunc();
    
      // Finalize does the following:
      //   - allocates virtual registers
      //   - inserts prolog / epilog
      //   - assembles to CodeHolder
      auto err = c.finalize();
      if (err) {
        printf("COMPILER FAILED: %s\b", DebugUtils::errorAsString(err));
        return 1;
      }
    
      typedef void (*EntryPoint)(void);
      EntryPoint entry;
    
      // Adds function to the runtime. Should be freed by rt.release().
      // Function is valid until the runtime is valid if not released.
      err = rt.add(&entry, &code);
      if (err) {
        printf("RUNTIME FAILED: %s\b", DebugUtils::errorAsString(err));
        return 1;
      }
    
      entry();
      return 0;
    }