Run-time mocking in C?

后端 未结 4 1974
旧巷少年郎
旧巷少年郎 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 <stdio.h>
    #include <stdlib.h>
    
    // Additional headers
    #include <stdint.h> // for uint32_t
    #include <sys/mman.h> // for mprotect
    #include <errno.h> // 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

    0 讨论(0)
  • 2021-02-08 14:51

    test-dept is a relatively recent C unit testing framework that allows you to do runtime stubbing of functions. I found it very easy to use - here's an example from their docs:

    void test_stringify_cannot_malloc_returns_sane_result() {
      replace_function(&malloc, &always_failing_malloc);
      char *h = stringify('h');
      assert_string_equals("cannot_stringify", h);
    }
    

    Although the downloads section is a little out of date, it seems fairly actively developed - the author fixed an issue I had very promptly. You can get the latest version (which I've been using without issues) with:

    svn checkout http://test-dept.googlecode.com/svn/trunk/ test-dept-read-only
    

    the version there was last updated in Oct 2011.

    However, since the stubbing is achieved using assembler, it may need some effort to get it to support ARM.

    0 讨论(0)
  • 2021-02-08 14:51

    An approach that I have used in the past that has worked well is the following.

    For each C module, publish an 'interface' that other modules can use. These interfaces are structs that contain function pointers.

    struct Module1 
    {
        int (*getTemperature)(void);
        int (*setKp)(int Kp);
    }
    

    During initialization, each module initializes these function pointers with its implementation functions.

    When you write the module tests, you can dynamically changes these function pointers to its mock implementations and after testing, restore the original implementation.

    Example:

    void mocked_dummy(void)
    {
        printf("__%s__()\n",__func__);
    }
    /*---- do not modify ----*/
    void dummyFn(void)
    {
        printf("__%s__()\n",__func__);
    }
    static void (*dummy)(void) = dummyFn;
    int factorial(int num)
    {
        int                      fact = 1;
            printf("__%s__()\n",__func__);
        while (num > 1)
        {
            fact *= num;
            num--;
        }
        dummy();
        return fact;
    }
    
    /*---- do not modify ----*/
    int main(int argc, char * argv[])
    {
        void (*oldDummy) = dummy;
    
    /* with the original dummy function */
        printf("factorial of 5 is = %d\n",factorial(5));
    
    /* with the mocked dummy */
        oldDummy = dummy;   /* save the old dummy */
        dummy = mocked_dummy; /* put in the mocked dummy */
        printf("factorial of 5 is = %d\n",factorial(5));
        dummy = oldDummy; /* restore the old dummy */
        return 1;
    }
    
    0 讨论(0)
  • 2021-02-08 14:51

    You can replace every function by the use of LD_PRELOAD. You have to create a shared library, which gets loaded by LD_PRELOAD. This is a standard function used to turn programs without support for SOCKS into SOCKS aware programs. Here is a tutorial which explains it.

    0 讨论(0)
提交回复
热议问题