打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
小x讲解C语言:常量字符串
计算机全栈工程师 2018-09-17 21:14:56

讲C语法的书实在太多了,讲C语言的语法从来不是本系列文章的目的,这里主要是通过一些例子展现操作系统、C编译器/链接器的一些特性。

看似简单的一段常量字符的代码也能引出许多值得研究的地方。

先看一段程序

#include<string.h>
char *string = "AABBCCAABBCCAABBCC";
int main(int argc,char *argv[]){
strncpy(string,"toutiao.com",sizeof("toutiao.com") );
return 0;
}

这段程序可以考察开发者是否知道在某些计算机架构和编译器下常量会被存放在只读的区域,如果对这个区域进行写操作进程就有可能崩溃。

从操作系统的角度去看:虽然 string 指向的 "AABBCCAABBCCAABBCC" 空间比 "toutiao.com" 要大,但 string 指向的这块内存所在页表是被设置为只读的,因此进程运行到strncpy就会崩溃。

再从别的角度去看这个问题,据说一些嵌入式系统的程序是烧录到rom里面的,这样常量字符串在程序运行时就可能是直接寻址到rom的地址,我们知道rom肯定是不可写的,程序运行到这里多数就是崩溃了。

上面这段程序运行结果是这样:

运行崩溃

为了深入研究这个问题,我们可以看一下Linux里编译好的程序结构:

gcc a.c
objdump -s a.out

objdump的结果

上面 objdump 输出的内容我去掉一大部分使重点突出

可以看到 "AABBCCAABBCCAABBCC" 是放在一个叫 .rodata 的区域里,ro 就是read only 的意思。

而在 macOS 下则是放在一个叫 __cstring 的区域

macOS下的objdump输出内容

从上面 macOS 的可执行文件中我们还看到 "toutiao.com" 这个字符串跟 "AABBCCAABBCCAABBCC" 是挨在一起的,那是因为macOS的编译器把代码里面"toutiao.com"的也作为常量处理了; 而我目前使用的Linux下的编译器,则把 "toutiao.com" 作为立即操作数 放进内存拷贝的过程中,在实际过程中,程序是没有跳转入 strncpy 的函数,因为对于可以预知长度的常量字符串拷贝,编译器在生成汇编代码时是有可能使用 buildin 函数替换掉 libc 里的 strncpy 函数,这是很常见的编译器优化手段,有兴趣的读者可以自行反汇编看一下。

在一些编译器下,把 char * 改为 const char * 结果是一样的,但如果是 const char * ,编译器就会知道 string 是一个常量指针,从而提示警告信息。

总之,我们只需知道在程序运行时常量字符串很可能被放在一个只读的内存页,用户进程企图修改这块内存通常就会引发崩溃。

理解了上面规则后,我们可以更深一点去体会现代操作系统、进程、内存管理的细节。

我们想一下:我们是不是就不能修改 string 指向的这块内存呢?实际情况是:这要跟具体的操作系统,编译器有关,Linux,macOS(理论上FreeBSD也可以)是可以的,至于 Windows 就不清楚了。

上面提到 string 指向的这块内存所在的页表是被设置为只读的,那么通过 Linux 和 BSD 提供的 mprotect 系统调用把这块内存改为可写后这块内存可以修改了;

mprotect 系统调用的原型是这样:

int mprotect(void *addr, size_t len, int prot);

传入的内存地址 addr 有一个要求,就是这块内存页起始地址,否则调用是不会成功的,我们要考虑到 string 指向的地址不一定内存页的起始地址,事实上在Linux和macOS上观察到 string指向的内存 跟代码段连在一起的。

因此,我们要先根据 string 指向的地址 算出这块内存所在的内存页的起始地址,而我们要根据一地址算出所在的内存页的起始地址就必须知道当前的OS的内存页的大小,就是PAGE_SIZE。

虽然我们知道目前大部分操作系统的PAGE_SIZE都是4096,但是严谨的角度,还检验一下,为了简化一下就不通过写代码获得这个PAGE_SIZE,有一个命令可以直接查看

getconf PAGE_SIZE
4096

Linux/macOS都有getconf这个命令。

然后,准备一组宏去计算内存页的起始地址

#define PAGE_SIZE 4096UL
#define PAGE_MASK (~(PAGE_SIZE-1))
#define PAGE_ALIGN(addr) (((addr)+PAGE_SIZE-1)&PAGE_MASK)

完整的代码如下,为了验证结果正确,最后加入了打印字符串的代码

#include <stdio.h>
#include<string.h>
#include <sys/mman.h>
#define PAGE_SIZE 4096UL
#define PAGE_MASK (~(PAGE_SIZE-1))
#define PAGE_ALIGN(addr) (((addr)+PAGE_SIZE-1)&PAGE_MASK)
char *string="AABBCCAABBCCAABBCC";
int main(int argc,char *argv[]){
void *start_of_page = (void *)(PAGE_ALIGN((long)string) - PAGE_SIZE);
int ret = mprotect(start_of_page, PAGE_SIZE, PROT_READ|PROT_WRITE|PROT_EXEC);
strncpy(string,"toutiao.com",sizeof("toutiao.com") );
printf("string[%s]",string);
return 0;
}

运行结果如下:

程序不再崩溃,内容拷贝进去

这里还有一个地方需要解释一下,不是说通过 mprotect 把内存页改为可写就可以吗?为什么还要加上 PROT_EXEC 呢?

这个问题也比较好理解,因为上面提到在Linux和macOS上可以观察到 string 指向的内存 跟 代码段连在一起的,如果把这内存页去掉了 PROT_EXEC 权限,这页内存里面代码就无法运行了,因此这当然需要保留PROT_EXEC权限了。

一段小小的程序能引出那么多话题,这是因为C语言跟从操作系统紧密联系在一起的,如果学习C语言只学习语法不去了解多一点编译器/链接器和操作系统相关的知识,那么会遇到许多稀奇古怪的问题而不得其解的,几乎不可能在实际工作使用好C语言。

小x全栈工程师原创  

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
“C/C 中char* 和 char「」区别
关于常量和变量
[收藏]C++ Tips(1)--const - 心如止水--coofucoo的专栏
deprecated conversion from string constant to 'char *'
Java String 对象,你真的了解了吗?
指针与数组的区别
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服