Run-time mocking in C?

后端 未结 4 1980
旧巷少年郎
旧巷少年郎 2021-02-08 14:20

This has been pending for a long time in my list now. In brief - I need to run mocked_dummy() in the place of dummy() ON RUN-TIME, wit

4条回答
  •  悲&欢浪女
    2021-02-08 14:39

    This is a question I've been trying to answer myself. I also have the requirement that I want the mocking method/tools to be done in the same language as my application. Unfortunately this cannot be done in C in a portable way, so I've resorted to what you might call a trampoline or detour. This falls under the "Make the code self modifiable." approach you mentioned above. This is were we change the actually bytes of a function at runtime to jump to our mock function.

    #include 
    #include 
    
    // Additional headers
    #include  // for uint32_t
    #include  // for mprotect
    #include  // for errno
    
    void mocked_dummy(void)
    {
        printf("__%s__()\n",__func__);
    }
    
    /*---- do not modify ----*/
    void dummy(void)
    {
        printf("__%s__()\n",__func__);
    }
    
    int factorial(int num) 
    {
        int                      fact = 1;
        printf("__%s__()\n",__func__);
        while (num > 1)
        {
            fact *= num;
            num--;
        }
        dummy();
        return fact;
    }
    /*---- do not modify ----*/
    
    typedef void (*dummy_fun)(void);
    
    void set_run_mock()
    {
        dummy_fun run_ptr, mock_ptr;
        uint32_t off;
        unsigned char * ptr, * pg;
    
        run_ptr = dummy;
        mock_ptr = mocked_dummy;
    
        if (run_ptr > mock_ptr) {
            off = run_ptr - mock_ptr;
            off = -off - 5;
        }
        else {
            off = mock_ptr - run_ptr - 5;
        }
    
        ptr = (unsigned char *)run_ptr;
    
        pg = (unsigned char *)(ptr - ((size_t)ptr % 4096));
        if (mprotect(pg, 5, PROT_READ | PROT_WRITE | PROT_EXEC)) {
            perror("Couldn't mprotect");
            exit(errno);
        }
    
        ptr[0] = 0xE9; //x86 JMP rel32
        ptr[1] = off & 0x000000FF;
        ptr[2] = (off & 0x0000FF00) >> 8;
        ptr[3] = (off & 0x00FF0000) >> 16;
        ptr[4] = (off & 0xFF000000) >> 24;
    }
    
    int main(int argc, char * argv[])
    {
        // Run for realz
        factorial(5);
    
        // Set jmp
        set_run_mock();
    
        // Run the mock dummy
        factorial(5);
    
        return 0;
    }
    

    Portability explanation...

    mprotect() - This changes the memory page access permissions so that we can actually write to memory that holds the function code. This isn't very portable, and in a WINAPI env, you may need to use VirtualProtect() instead.

    The memory parameter for mprotect is aligned to the previous 4k page, this also can change from system to system, 4k is appropriate for vanilla linux kernel.

    The method that we use to jmp to the mock function is to actually put down our own opcodes, this is probably the biggest issue with portability because the opcode I've used will only work on a little endian x86 (most desktops). So this would need to be updated for each arch you plan to run on (which could be semi-easy to deal with in CPP macros.)

    The function itself has to be at least five bytes. The is usually the case because every function normally has at least 5 bytes in its prologue and epilogue.

    Potential Improvements...

    The set_mock_run() call could easily be setup to accept parameters for reuse. Also, you could save the five overwritten bytes from the original function to restore later in the code if you desire.

    I'm unable to test, but I've read that in ARM... you'd do similar but you can jump to an address (not an offset) with the branch opcode... which for an unconditional branch you'd have the first bytes be 0xEA and the next 3 bytes are the address.

    Chenz

提交回复
热议问题