在内存中复制包含静态变量的函数
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
变量。当使用 mmap
和 memcpy
在动态位置复制 foo
时,复制的偏移量保持不变,但 rip
的值在该指令时执行起来会有很大的不同。
更重要的是,rip+0x2e3d
的计算地址可能最终位于未标记为可供进程使用的内存区域,访问该地址将导致分段错误。在 0x11da
和 0x11e0
的后续访问将导致相同的问题。
继续,即使您将静态变量 i
更改为局部堆栈变量,您仍然会 运行 进入问题,如地址 0x11f4
相对形式CALL
instruction 的用于调用 printf
,偏移量为 0x10b0
,这也会导致分段错误,原因相同。
如果你能纠正这个问题,在 foo
函数中还有一个相对寻址计算,在地址 0x11e8
,它使用 LEA
instruction 来计算地址"%d\n"
格式字符串。计算本身没有问题,但是对该计算地址的任何访问也可能会导致分段错误。
我在搞乱函数指针,我想做的是在内存中复制一个函数,因此我使用 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
变量。当使用 mmap
和 memcpy
在动态位置复制 foo
时,复制的偏移量保持不变,但 rip
的值在该指令时执行起来会有很大的不同。
更重要的是,rip+0x2e3d
的计算地址可能最终位于未标记为可供进程使用的内存区域,访问该地址将导致分段错误。在 0x11da
和 0x11e0
的后续访问将导致相同的问题。
继续,即使您将静态变量 i
更改为局部堆栈变量,您仍然会 运行 进入问题,如地址 0x11f4
相对形式CALL
instruction 的用于调用 printf
,偏移量为 0x10b0
,这也会导致分段错误,原因相同。
如果你能纠正这个问题,在 foo
函数中还有一个相对寻址计算,在地址 0x11e8
,它使用 LEA
instruction 来计算地址"%d\n"
格式字符串。计算本身没有问题,但是对该计算地址的任何访问也可能会导致分段错误。