在 Linux C 程序中,有没有一种方法可以在运行时修改函数的代码?

3

简单来说,我们有两个类似的函数:

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

现在我们在主函数中调用f1,它会打印出123。编译后,f1的反汇编可能像这样:
```assembly mov eax, 123 ret ```
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的机器码替换f1。我使用memcpy(f1, f2, SIZE_OF_F2_MACHINE_CODE)。但是出现了问题——段错误。

现在我想知道是否存在解决这个问题的方法。这是一个常见的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程序中工作。那么什么可以工作呢?


2
搜索“自修改代码”和“内存保护”。基本上,您不能仅仅修改程序代码,因为它很可能是只读的。 - Mysticial
1
如果你想改变程序执行的代码,你可以动态地加载(和卸载)带有不同代码的共享库。 - Jonathan Leffler
实际上,我想更改Linux内核函数的代码。我怀疑它是否可以卸载。 - terry
"ksplice"(http://en.wikipedia.org/wiki/Ksplice)修改正在运行的内核。内核支持已包含在主线中,因此您可以像它一样对正在运行的内核进行修改。(例如,请参见http://knoppix.mirrors.tds.net/pub/linux/frugalware/frugalware-1.6/source/apps-extra/ksplice/ksplice-0.9.9-src.tar.gz。) - Nominal Animal
请注意,即使是非常相似的源代码,函数的机器大小也可能会有所不同。也许将“123”更改为“12345”可能会改变其大小(例如,因为“123”适合一个字节,但“12345”不适合,因此需要不同的机器指令)。 - Basile Starynkevitch
3个回答

3

不要覆盖过程,而应该覆盖符号表中的符号引用。这需要动态链接。或者您可以用对另一个函数的调用来覆盖对该函数的调用,但是像NX位这样的东西可能会阻碍您的方式。自修改代码通常不受欢迎。


3
如果您的目的仅是最终能够调用由相同过程生成的某些函数的代码,那么为什么要这样问呢?您可以采取不同的方法:
  1. 始终使用函数指针来调用这些动态生成的函数;我的建议是,出于可读性的考虑,在声明指针之前使用typedef定义它们的签名,参见this answer
  2. 生成函数并获取其指针。

    • 例如,您可以生成一个C源文件generated.c,fork一个进程,可能使用system("gcc -fPIC -O -shared generated.c -o generated.so");编译它,然后使用dlopen("./generated.so", RTLD_GLOBAL)打开它,并使用dlsym获取生成函数的指针。有关详细信息,请参见dlopen(3)手册页面。FYI,MELT正在执行此操作。

    • 您还可以在内存中生成函数的机器代码(可能使用PROT_EXEC标志使用mmap(2))。有几个JIT(即时翻译)库可用:GNU lightning(快速生成慢运行的机器代码),myjitlibjitLLVM(生成优化的机器代码较慢),LuaJIT...

如果你真的希望覆盖一些现有的函数代码,你可以这样做,但需要非常小心并且很痛苦(例如因为新函数代码需要比旧的更多的空间,还有由于重定位问题)。使用mmap(2)和/或mprotect(2)系统调用来获取执行此类技巧的权限。但要准备好进行调试噩梦。你可能想要使用你的python脚本编写gdb调试器脚本。
对于内核模块,情况就不同了。我听说一些网络相关的内核代码(也许是iptables?)可能使用JIT技术生成机器代码并运行它。

另一个要点是,如果您的代码使用-fPIC编译,则在内存位置之间复制相同的代码可能无法正常工作。函数和静态变量的地址是相对于指令指针(也称为程序计数器)计算的。 - ugoren

2

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

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()"开始之前有足够的代码)。如果你使用的是gcc 4.3及以上版本,可以使用属性(因为我使用的是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 )"。它用于区分应该被替换的内容(打印出来的数字)和不应该被替换的内容(相对引用)。显然,这是一个非常简化的算法。你需要实现适合自己任务的算法。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接