系统程序员成长计划-你的数据放在哪里(下)
转载时请注明出处和作者联系方式
文章出处:http://www.limodev.cn/blog
作者联系方式:李先静 <xianjimli at hotmail dot com>
对 于初学者来说这道题有点难度,很少有人能完全做对的。不过没关系,我们的目标并不是要难倒读者,而是要刺激读者去思考,加深学习的印象。有了前面两次的经 验,我想没有人再去写一个dlist_to_upper的函数了,大家都会调用dlist_foreach来实现。不过新的问题又出现了,初学者常犯的错 误有以下几种:
1. 转换大写的方法不对。
char* p = str; if(p != NULL) { while(*p != '/0') { if('a' <= *p && *p <= 'z') { *p = *p - ('a' - 'A'); } p++; } }
这是我们在课本里学到的写法,但在工程中是不能这样做的。因为大小写字母在不同语言中的定义是不一样的,’a’是一个字符常量,它的值在任何时候都 是97,但在不同语言中,97却不一定代表’a’。我们不能简单的认为在97(‘a’)-122(‘z’)之间的字符就是小写字母,而是应该调用标准C函 数islower来判断,同样转换为大写应该调用toupper而不是减去一个常量。
2. 在双向链表中存放常量字符串,转换时出现段错误。
DList* dlist = dlist_create(); dlist_append(dlist, "It"); dlist_append(dlist, "is"); dlist_append(dlist, "OK"); dlist_append(dlist, "!"); dlist_foreach(dlist, str_toupper, NULL); dlist_destroy(dlist);
运行时会出现Segmentation fault错误。原因是”It”等字符串是常量,常量是不能修改的。
3. 在双向链表中存放的是临时变量,转换后发现所有字符串都一样。
char str[256] = {0}; DList* dlist = dlist_create(); strcpy(str, "It"); dlist_append(dlist, str); strcpy(str, "is"); dlist_append(dlist, str); strcpy(str, "OK"); dlist_append(dlist, str); strcpy(str, "!"); dlist_append(dlist, str); dlist_foreach(dlist, str_toupper, NULL); dlist_foreach(dlist, str_print, NULL); dlist_destroy(dlist);
运行时发现打印出几个感叹号。原因是dlist_append时没有拷贝一份,所以在dlist中存放的是同一个地址。而且这个dlist在当前函数返回后,里面保存的数据都无效了,因为这些数据指向的是临时变量。
4. 存放时拷贝了数据,但没有free分配的内存。
DList* dlist = dlist_create(); dlist_append(dlist, strdup("It")); dlist_append(dlist, strdup("is")); dlist_append(dlist, strdup("OK")); dlist_append(dlist, strdup("!")); dlist_foreach(dlist, str_toupper, NULL); dlist_foreach(dlist, str_print, NULL); dlist_destroy(dlist);
这里看起来工作正常了,但存在内存泄露的BUG。strdup调用malloc分配了内存,但没有地方去free它们。
初学者对内存和指针只有一知半解的认识,常常犯一些连自己都莫名其妙的错误。为了避免这些不必要的错误,今天我们要学习各种数据存放的位置以及它们的特性,让初学者对编程有更进一步的认识。在程序中,数据存放的位置主要有以下几个:
1.未初始化的全局变量(.bss段)
已经记不清bss代表Block Storage Start还是Block Started by Symbol。像我这种没有用过那些史前计算机的人,终究无法明白这样怪异的名字,记不住也是不足为奇的。不过没有关系,重要的是,我们要清楚什么数据是 存放在bss段中的,这些数据有什么样的特点以及如何使用它们。
通俗的说,bss段是用来存放那些没有初始化的和初始化为0的全局变量的。它有什么特点呢,让我们来看看一个小程序的表现。
int bss_array[1024 * 1024]; int main(int argc, char* argv[]) { return 0; } # gcc -g bss.c -o bss.exe # ls -l bss.exe -rwxrwxr-x 1 root root 5975 Nov 16 09:32 bss.exe # objdump -h bss.exe |grep bss 24 .bss 00400020 080495e0 080495e0 000005e0 2**5
变量bss_array的大小为4M,而可执行文件的大小只有5K。 由此可见,bss类型的全局变量只占运行时的内存空间,而不占用文件空间。
现代大多数操作系统,在加载程序时,会把所有的bss全局变量清零。但为保证程序的可移植性,手工把这些变量初始化为0也是一个好习惯,这样这些变量都有个确定的初始值。
当然作为全局变量,在整个程序的运行周期内,bss数据是一直存在的。
2.初始化过的全局变量 (.data段)
与bss相比,data段就容易明白多了,它的名字就暗示着里面存放着数据。当然,如果数据全是零,为了优化考虑,编译器把它当作bss处理。通俗的说,data段用来存放那些初始化为非零的全局变量。它有什么特点呢,我们还是来看看一个小程序的表现。
int data_array[1024 * 1024] = {1}; int main(int argc, char* argv[]) { return 0; } # ls -l data.exe -rwxrwxr-x 1 root root 4200313 Nov 16 09:34 data.exe # objdump -h data.exe |grep //.data 23 .data 00400020 080495e0 080495e0 000005e0 2**5
仅仅是把初始化的值改为非零了,文件就变为4M多。由此可见,data类型的全局变量是即占文件空间,又占用运行时内存空间的。
同样作为全局变量,在整个程序的运行周期内,data数据是一直存在的。
3.常量数据 (.rodata段)
rodata的意义同样明显,ro代表read only,rodata就是用来存放常量数据的。关于rodata类型的数据,要注意以下几点:
o 常量不一定就放在rodata里,有的立即数直接和指令编码在一起,存放在代码段(.text)中。
o 对于字符串常量,编译器会自动去掉重复的字符串,保证一个字符串在一个可执行文件(EXE/SO)中只存在一份拷贝。
o rodata是在多个进程间是共享的,这样可以提高运行空间利用率。
o 在有的嵌入式系统中,rodata放在ROM(或者norflash)里,运行时直接读取,无需加载到RAM内存中。
o 在嵌入式linux系统中,也可以通过一种叫作XIP(就地执行)的技术,也可以直接读取,而无需加载到RAM内存中。
o 常量是不能修改的,修改常量在linux下会出现段错误。
由此可见,把在运行过程中不会改变的数据设为rodata类型的是有好处的:在多个进程间共享,可以大大提高空间利用率,甚至不占用RAM空间。同 时由于rodata在只读的内存页面(page)中,是受保护的,任何试图对它的修改都会被及时发现,这可以提高程序的稳定性。
字符串会被编译器自动放到rodata中,其它数据要放到rodata中,只需要加const关键字修饰就好了。
4.代码 (.text段)
text段存放代码(如函数)和部分整数常量,它与rodata段很相似,相同的特性我们就不重复了,主要不同在于这个段是可以执行的。
5. 栈(stack)
栈用于存放临时变量和函数参数。栈作为一种基本数据结构,我并不感到惊讶,用来实现函数调用,这也司空见惯的作法。直到我试图找到另外一种方式实现递归操作时,我才感叹于它的巧妙。要实现递归操作,不用栈不是不可能,只是找不出比它更优雅的方式。
尽管大多数编译器在优化时,会把常用的参数或者局部变量放入寄存器中。但用栈来管理函数调用时的临时变量(局部变量和参数)是通用做法,前者只是辅助手段,且只在当前函数中使用,一旦调用下一层函数,这些值仍然要存入栈中才行。
通常情况下,栈向下(低地址)增长,每向栈中PUSH一个元素,栈顶就向低地址扩展,每从栈中POP一个元素,栈顶就向高地址回退。一个有兴趣的问 题:在x86平台上,栈顶寄存器为ESP,那么ESP的值在是PUSH操作之前修改呢,还是在PUSH操作之后修改呢?PUSH ESP这条指令会向栈中存入什么数据呢?据说x86系列CPU中,除了286外,都是先修改ESP,再压栈的。由于286没有CPUID指令,有的OS用 这种方法检查286的型号。
要注意的是,存放在栈中的数据只在当前函数及下一层函数中有效,一旦函数返回了,这些数据也自动释放了,继续访问这些变量会造成意想不到的错误。
6.堆(heap)
堆是最灵活的一种内存,它的生命周期完全由使用者控制。标准C提供几个函数:
malloc 用来分配一块指定大小的内存。
realloc 用来调整/重分配一块存在的内存。
free 用来释放不再使用的内存。
…
使用堆内存时请注意两个问题:
alloc/free要配对使用。内存分配了不释放我们称为内存泄露(memory leak),内存泄露多了迟早会出现Out of memory的错误,再分配内存就会失败。当然释放时也只能释放分配出来的内存,释放无效的内存,或者重复free都是不行的,会造成程序crash。
分配多少用多少。分配了100字节就只能用100字节,不管是读还是写,都只能在这个范围内,读多了会读到随机的数据,写多了会造成的随机的破坏。这种情况我们称为缓冲区溢出(buffer overflow),这是非常严重的,大部分安全问题都是由缓冲区溢出引起的。
手工检查有没有内存泄露或者缓冲区溢出是很困难的,幸好有些工具可以使用,比如linux下有valgrind,它的使用方法很简单,大家下去可以试用一下,以后每次写完程序都应该用valgrind跑一遍。
最后,我们来看看在linux下,程序运行时空间的分配情况:
# cat /proc/self/maps 00110000-00111000 r-xp 00110000 00:00 0 [vdso] 009ba000-009d6000 r-xp 00000000 08:01 768759 /lib/ld-2.8.so 009d6000-009d7000 r--p 0001c000 08:01 768759 /lib/ld-2.8.so 009d7000-009d8000 rw-p 0001d000 08:01 768759 /lib/ld-2.8.so 009da000-00b3d000 r-xp 00000000 08:01 768760 /lib/libc-2.8.so 00b3d000-00b3f000 r--p 00163000 08:01 768760 /lib/libc-2.8.so 00b3f000-00b40000 rw-p 00165000 08:01 768760 /lib/libc-2.8.so 00b40000-00b43000 rw-p 00b40000 00:00 0 08048000-08050000 r-xp 00000000 08:01 993652 /bin/cat 08050000-08051000 rw-p 00007000 08:01 993652 /bin/cat 0805f000-08080000 rw-p 0805f000 00:00 0 [heap] b7fe8000-b7fea000 rw-p b7fe8000 00:00 0 bfee7000-bfefc000 rw-p bffeb000 00:00 0 [stack]
每个区间都有四个属性:
r 表示可以读取。
w 表示可以修改。
x 表示可以执行。
p/s 表示是否为共享内存。
有文件名的内存区间,属性为r—p表示存放的是rodata。
有文件名的内存区间,属性为rw-p表示存放的是bss和data
有文件名的内存区间,属性为r-xp表示存放的是text数据。
没有文件名的内存区间,表示用mmap映射的匿名空间。
文件名为[stack]的内存区间表示是栈。
文件名为[heap]的内存区间表示是堆。
对内存的掌握是系统程序员必备的技能,希望读者多加体会。本节示例代码请到这里下载。