关于程序内存布局的问题在面试中经常会被问到,其中尤其常见的是问字符串常量到底存在哪个地方,是堆区,栈区,还是全局变量区?一般答案都会说是全局变量区,但是在我尝试更改字符串常量的值的时候却引发了一些问题。首先我尝试了使用VirtualProtect更改Windows下的字符串常量值的读写权限,很成功的更改了其值,但是在对Linux进行处理时则不然,使用先是mprotect失败,原因为EINVAL,查询手册得到说明为,addr和len必须为内存页的整倍,修改了程序为操作整页内存后,直接报出了SIGSEGV,经过观察发现,字符串常量在linux的虚拟内存中(起码是Ubuntu64位中)并没有存在只读数据rodata段,而是存在代码text段,因此使用mprotect将这一页的权限更改为READ|WRITE后,会将原有的EXEC属性去掉,因此程序无法继续执行,立刻报出了SIGSEGV。
首先一点小细节的知识,C和C++对类型的限制不同,C++对类型有较强的限制如代码
char* a = "abcdefg";
在VS环境下C++编译是不过的,g++编译会提示ISO C++禁止将const char*赋值给char*,
而将文件名重命名为.c后或是手动设置按照C编译,则二者都无任何提示编译通过。
那么字符串常量到底存在哪儿呢?
对于linux中并不是存在全局变量区,而是存在代码段中(64位linux,32位未验证),考虑如下代码
#include <stdio.h>
#include <errno.h>
#include <sys/mman.h>
#include <unistd.h>
int d = 1000;
int main()
{
char* h = "hello world.\n";
char a[] = "hello.\n";
int b = 10;
static int c = 100;
printf("%llx\n%llx\n%llx\n%llx\n%llx\n",a,&b,&c,&d,h);
return 0;
}
在Ubuntu上运行后得到结果
7ffd6da62cf0
7ffd6da62cdc
5578d572e014
5578d572e010
5578d552d834
一般来说a、b存在栈区,c、d存在静态变量区
对照/proc/[pid]/maps下的进程内存信息可发现,字符串常量h的地址位于第一个段中,而该段就是代码段,其权限为r-x。
(但是使用objdump查看a.out的信息却会看到这段字符串常量在elf文件中位于rodata段)
5578d552d000-5578d552e000 r-xp 00000000 08:10 14 /home/asd/1/a.out
5578d572d000-5578d572e000 r--p 00000000 08:10 14 /home/asd/1/a.out
5578d572e000-5578d572f000 rw-p 00001000 08:10 14 /home/asd/1/a.out
5578d75e5000-5578d7606000 rw-p 00000000 00:00 0 [heap]
7f822b9db000-7f822bbc2000 r-xp 00000000 08:02 3282198 /lib/x86_64-linux-gnu/libc-2.27.so
7f822bbc2000-7f822bdc2000 ---p 001e7000 08:02 3282198 /lib/x86_64-linux-gnu/libc-2.27.so
7f822bdc2000-7f822bdc6000 r--p 001e7000 08:02 3282198 /lib/x86_64-linux-gnu/libc-2.27.so
7f822bdc6000-7f822bdc8000 rw-p 001eb000 08:02 3282198 /lib/x86_64-linux-gnu/libc-2.27.so
7f822bdc8000-7f822bdcc000 rw-p 00000000 00:00 0
7f822bdcc000-7f822bdf3000 r-xp 00000000 08:02 3282170 /lib/x86_64-linux-gnu/ld-2.27.so
7f822bfd8000-7f822bfda000 rw-p 00000000 00:00 0
7f822bff3000-7f822bff4000 r--p 00027000 08:02 3282170 /lib/x86_64-linux-gnu/ld-2.27.so
7f822bff4000-7f822bff5000 rw-p 00028000 08:02 3282170 /lib/x86_64-linux-gnu/ld-2.27.so
7f822bff5000-7f822bff6000 rw-p 00000000 00:00 0
7ffd6da43000-7ffd6da64000 rw-p 00000000 00:00 0 [stack]
7ffd6db24000-7ffd6db27000 r--p 00000000 00:00 0 [vvar]
7ffd6db27000-7ffd6db29000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
因此如果需要修改字符串常量的内容,就需要一些处理,使用mprotect更改内存页的权限,该系统调用要求其操作必须为整页,即addr必须是系统页的整数倍,len也要是系统页的整数倍才行,该值可使用getpagesize()获取,具体修改字符串常量的代码则如下
#include <stdio.h>
#include <errno.h>
#include <sys/mman.h>
#include <unistd.h>
int main()
{
char* h = "hello world.\n";
unsigned long long address = (unsigned long long)(h);
address = address/4096*4096;
int result = mprotect((void*)(address),4096,PROT_EXEC|PROT_READ|PROT_WRITE);
printf ("%d\n",result);
h[5] = 'A';
printf(h);
return 0;
}
值得一提的是,对于Windows来说就没有那么麻烦,直接使用VirtualProtect修改字符串常量那段地址的权限即可,代码如下
#include <stdio.h>
#include <Windows.h>
int main()
{
char* a = const_cast<char*>("hello world.\n");
printf(a);
DWORD MemoryOldProtect = 0;
VirtualProtect(a, 13, PAGE_READWRITE, &MemoryOldProtect);
a[5] = 'A';
printf(a);
return 0;
}
查看MemoryOldProtect的值可知,原内存状态为0x02只读,可以推测Windows将字符串常量存到了rdata段,即只读变量区,使用x64dbg等工具可印证这点。