有没有办法在运行时修改Linux C程序中的函数的代码?

简单来说,我们有两个相似的function:

void f1() { printf("%d", 123); } void f2() { printf("%d", 124); } 

现在我们在main调用f1,它打印123.编译时, f1的反汇编可能是这样的:

 08048424 <f1>: 8048424: 55 push %ebp 8048425: 89 e5 mov %esp,%ebp 8048427: 83 ec 18 sub $0x18,%esp 804842a: b8 40 86 04 08 mov $0x8048640,%eax 804842f: c7 44 24 04 7b 00 00 movl $0x7b,0x4(%esp) 8048436: 00 8048437: 89 04 24 mov %eax,(%esp) 804843a: e8 05 ff ff ff call 8048344 <printf@plt> 804843f: c9 leave 8048440: c3 ret 

f2的机器代码与f1类似。

现在我想在运行时用f2的机器代码replacef1。 我使用memcpy(f1,f2,SIZE_OF_F2_MACHINE_CODE)。 当然,这是问题 – 一个段的错误。

现在我想知道是否有解决scheme来解决这个问题。 这是一个普通的C程序。 据我所知,我们可以使用下面的代码在Linux内核中设置页面可写

 int set_page_rw(long unsigned int addr) { unsigned int level; pte_t *pte = lookup_address(addr, &level); if(pte->pte & ~_PAGE_RW) pte->pte |= _PAGE_RW } 

但是它在正常的Linux C程序中不起作用。 那么什么工作?

    不要覆盖该过程,而是覆盖符号表中的符号引用。 这确实需要动态链接。 或者,您可以通过调用另一个函数来覆盖函数的调用,但NX位等可能会阻碍您的工作。 自我修改的代码通常是不被接受的。

    你为什么要问? 如果你的愿望只是最终能够调用一些代码由相同的进程生成的函数,你可以采取不同的方式:

    1. 总是使用函数指针来调用这些动态生成的函数; 我的建议是,为了可读性的原因,在声明指针前typedef自己的签名,看到这个答案 。
    2. 生成函数并获取指针。

      • 你可以例如生成一个C源文件generated.c ,fork一个进程,也许用system("gcc -fPIC -O -shared generated.c -o generated.so"); 编译它,然后dlopen("./generated.so", RTLD_GLOBAL)并用dlsym得到生成函数的指针。 有关详细信息,请参阅dlopen(3)手册页。 仅供参考, MELT正在这样做。

      • 您也可以在内存中生成函数的机器码(可能使用PROT_EXEC标志通过mmap(2)获得)。 几个JIT(即时翻译)库可用: GNU闪电 (快速生成慢速机器代码), myjit , libjit , LLVM (优化机器代码的慢生成), LuaJIT …

    如果你真的想覆盖一些现有的功能代码,你可能会这样做,但这需要大量的关心和痛苦(例如,因为新的功能代码需要比旧的更多的空间,也因为搬迁问题)。 使用mmap(2)和/或mprotect(2)系统调用来获得这些技巧的权限。 但准备调试噩梦。 你可能想用你的python脚本来编写你的gdb调试器。

    对于内核模块来说,故事是不同的。 我听说有些网络相关的内核代码( iptables也许?)可能使用JIT技术来生成机器代码并运行它。

    我试图找到你的答案,但失败了。 我实际上成功做的 – 只是简化有问题的代码:

     void f1( ) { } int main( ) { *(char*) f1 = *(char*) f1; return( 0 ); } 

    是的,它在分段错误(在gcc中)或内存访问冲突(在MS VC中)失败。

    编辑:

    其实我成功做你想做的事

    (根据Basile Starynkevitch的答案)。 但只适用于x86,仅适用于gcc,并且仅适用于您的具体示例。 以下是几个代码示例。

    首先 – 简单的例子。

     #include <stdio.h> #include <sys/mman.h> #include <unistd.h> void f1( ) { } int main( ) { int rc; int pagesize; char *p; f1( ); pagesize = sysconf( _SC_PAGE_SIZE ); printf( "pagesize=%d (0x%08X).\n", pagesize, pagesize ); if( pagesize == -1 ) return( 2 ); p = (char*) f1; printf( "p=0x%08X.\n", p ); p = (char*) ((size_t) p & ~(pagesize - 1)); printf( "p=0x%08X.\n", p ); rc = mprotect( p, pagesize, PROT_READ | PROT_WRITE | PROT_EXEC ); printf( "rc=%d.\n", rc ); if( rc != 0 ) return( 2 ); printf( "'mprotect()' succeeded.\n" ); *(char*) f1 = *(char*) f1; printf( "Write succeeded.\n" ); f1( ); printf( "Call succeeded.\n" ); return( 0 ); } 

    你编译这个并启动一次。 它会失败,但你会知道页面大小。 比方说,这是4096 。 然后你像这样编译这个例子:

     gcc a1.c -falign-functions=4096 

    它应该工作。

    输出:

     pagesize=4096 (0x00001000). p=0x00402000. p=0x00402000. rc=0. 'mprotect()' succeeded. Write succeeded. Call succeeded. 

    现在先进的例子:

     #include <stdio.h> #include <sys/mman.h> #include <unistd.h> #include <string.h> //extern void f1( void ) __attribute__(( aligned( 4096 ) )); __asm__( ".text" ); __asm__( ".align 4096" ); void f1( void ) { printf( "%d\n", 123 ); } void f2( void ) { printf( "%d\n", 124 ); } int main( void ) { int rc; int pagesize; char *p; int i; printf( "f1=0x%08X.\n", f1 ); printf( "f2=0x%08X.\n", f2 ); f1( ); f2( ); pagesize = sysconf( _SC_PAGE_SIZE ); printf( "pagesize=%d (0x%08X).\n", pagesize, pagesize ); if( pagesize == -1 ) return( 2 ); p = (char*) f1; printf( "p=0x%08X.\n", p ); p = (char*) ((size_t) p & ~(pagesize - 1)); printf( "p=0x%08X.\n", p ); rc = mprotect( p, pagesize, PROT_READ | PROT_WRITE | PROT_EXEC ); printf( "rc=%d.\n", rc ); if( rc != 0 ) return( 2 ); printf( "'mprotect()' succeeded.\n" ); for( i = 0; i < (size_t) f2 - (size_t) f1; i++ ) { if( ((char*) f2)[ i ] == 124 ) { printf( "i=%d.\n", i ); ((char*) f1)[ i ] = ((char*) f2)[ i ]; } } //memcpy( f1, f2, (size_t) f2 - (size_t) f1 ); printf( "Write succeeded.\n" ); f1( ); f2( ); printf( "Call succeeded.\n" ); return( 0 ); } 

    你不能在这里使用“ memcpy() ”,因为在“ f1() ”和“ f2() ”中调用“ printf () ”是相对的,而不是绝对的。 而我无法找到如何使它们绝对(在我的情况下,“既不-fPIC ”也不是“ -fno-PIC ”)。 如果你在“ f1() ”和“ f2() ”中没有相对的函数调用,我相信你可以使用“ memcpy() ”(但我没有尝试)。

    您还应该使用“ f1() ”对齐页面大小(除非您确定在“ f1() ”开始之前有足够的代码)。 如果你有4.3及更高版本的gcc,你可以使用属性(这是评论,因为我有gcc v4.1.2)。 如果没有,你可以使用那个丑陋而不可靠的“ _asm _ ”。

    输出:

     f1=0x00402000. f2=0x0040201E. 123 124 pagesize=4096 (0x00001000). p=0x00402000. p=0x00402000. rc=0. 'mprotect()' succeeded. i=12. Write succeeded. 124 124 Call succeeded. 

    当然,那个可怕的“ if(((char *)f2)[i] == 124) ”。 它用于区分应该替换的内容(打印的数字)和不应该的内容(相对引用)。 显然,这是非常简单的算法。 你将不得不实现你自己的,适合你的任务。