在内存中复制包含静态变量的函数

Duplicating a function containing static variables in memory

我在搞乱函数指针,我想做的是在内存中复制一个函数,因此我使用 mmap 来执行我的内存,然后 memcpy 在我的新指针中复制函数,我想通过这样做实现的是在我的函数内部创建一个静态变量的新实例。问题是我在复制函数时做错了什么,因为当我尝试调用函数时,它出现了段错误。这是代码:

#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>

int     g_pagesize;

void    foo(void)
{
    static int i = 0;

    i++;
    printf("%d\n", i);
}

void    bar(void)
{
    printf("some stuff\n");
}

int main(void)
{
    void    (*fp1)(void), (*fp2)(void);

    g_pagesize = getpagesize();
    fp1 = mmap(NULL, g_pagesize, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    fp2 = mmap(NULL, g_pagesize, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    memcpy(fp1, foo, bar - foo);
    memcpy(fp2, foo, bar - foo);
    for (int i = 0; i < 5; i++)
        (*fp1)();
    for (int i = 0; i < 5; i++)
        (*fp2)();
    return (0);
}

/*
** Wanted output:
** 1
** 2
** 3
** 4
** 5
** 1
** 2
** 3
** 4
** 5
*/

如评论中所述,这将不起作用,因为 i 变量只有一个实例,存储在 .bss segment 中,所有 static 都是这种情况变量。因此,即使 foo 的代码以 运行 而不会导致分段错误的方式进行复制,它也只会从 1 递增到 10。

这个问题中更有趣的部分是那些分段错误,以及它们发生的原因。

为了考虑发生了什么,让我们看一下 foo 函数的一些(相对未优化的)x86-64 汇编输出,原始源代码交错:

00000000000011c9 <foo>:
# void    foo(void)
# {
    11c9:       f3 0f 1e fa             endbr64
    11cd:       55                      push   rbp
    11ce:       48 89 e5                mov    rbp,rsp
#     static int i = 0;
#
#     i++;
    11d1:       8b 05 3d 2e 00 00       mov    eax,DWORD PTR [rip+0x2e3d]        # 4014 <i.3666>
    11d7:       83 c0 01                add    eax,0x1
    11da:       89 05 34 2e 00 00       mov    DWORD PTR [rip+0x2e34],eax        # 4014 <i.3666>
#     printf("%d\n", i);
    11e0:       8b 05 2e 2e 00 00       mov    eax,DWORD PTR [rip+0x2e2e]        # 4014 <i.3666>
    11e6:       89 c6                   mov    esi,eax
    11e8:       48 8d 3d 15 0e 00 00    lea    rdi,[rip+0xe15]        # 2004 <_IO_stdin_used+0x4>
    11ef:       b8 00 00 00 00          mov    eax,0x0
    11f4:       e8 b7 fe ff ff          call   10b0 <printf@plt>
# }
    11f9:       90                      nop
    11fa:       5d                      pop    rbp
    11fb:       c3                      ret

在地址 0x11d1 处,MOV instruction 的相对形式用于将 i 的当前值加载到 eax 寄存器中。如您所见,助记符的形式为:

mov    eax,DWORD PTR [rip+<offset>]

可以读作:

  • 根据当前指令指针(rip)加上提供的偏移量计算出一个地址,然后
  • 将存储在那里的 32 位值 (DWORD) 复制到 eax 寄存器中

此偏移值 (0x2e3d) 是在编译时计算的,因此在执行指令时,rip 恰好 0x2e3d 小于静态 i变量。当使用 mmapmemcpy 在动态位置复制 foo 时,复制的偏移量保持不变,但 rip 的值在该指令时执行起来会有很大的不同。

更重要的是,rip+0x2e3d 的计算地址可能最终位于未标记为可供进程使用的内存区域,访问该地址将导致分段错误。在 0x11da0x11e0 的后续访问将导致相同的问题。

继续,即使您将静态变量 i 更改为局部堆栈变量,您仍然会 运行 进入问题,如地址 0x11f4 相对形式CALL instruction 的用于调用 printf,偏移量为 0x10b0,这也会导致分段错误,原因相同。

如果你能纠正这个问题,在 foo 函数中还有一个相对寻址计算,在地址 0x11e8,它使用 LEA instruction 来计算地址"%d\n" 格式字符串。计算本身没有问题,但是对该计算地址的任何访问也可能会导致分段错误。