系统程序员成长计划-你的数据放在哪里(下)
2,178 views| 2008-11-24| 李先静| 系统程序员成长计划| | 8 条评论转载时请注明出处和作者联系方式
文章出处:http://www.limodev.cn/blog
作者联系方式:李先静 <xianjimli@gmail.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]的内存区间表示是堆。
对内存的掌握是系统程序员必备的技能,希望读者多加体会。本节示例代码请到这里下载。
系统程序员成长计划 Share
Comments
Tags
Recent Posts
Most Viewed
- 系统程序员成长计划写作提纲 - 19,605 views
- Android IPC机制详解 - 6,277 views
- 系统程序员成长计划-走近专业程序员(上) - 6,253 views
- 系统程序员成长计划-写得又快又好的秘诀(一) - 5,390 views
- 系统程序员成长计划-背景知识 - 5,070 views
- i++循环与i–循环的执行效率 - 4,712 views
- 系统程序员成长计划-Write once, run anywhere(WORA)(上) - 4,700 views
- 系统程序员成长计划-走近专业程序员(下) - 4,254 views
- Linux下的调试工具 - 4,017 views
- Advanced Linux Sound Architecture (ALSA) 研究笔记 - 4,017 views
- 系统程序员成长计划-序 - 3,985 views
- 系统程序员成长计划-写得又快又好的秘诀(三) - 3,929 views
- 中国人与自由软件文化研究(搞笑版) - 3,735 views
- Android中的MessageQueue,Handler,Looper和Thread - 3,686 views
- 答复:我不会OOO,仍然可以XXX - 3,658 views
Categories
- Android (28)
- Broncho-A1-Hack (6)
- DirectFB (7)
- FTK(嵌入式GUI) (24)
- GTK+ (29)
- KVM hack notes (8)
- Linux Mobile (65)
- Management (5)
- Mozilla (9)
- Open Source (5)
- Programming (34)
- Tools (9)
- Uncategorized (23)
- Win32 (3)
- X Windows (31)
- 沉思录 (29)
- 系统程序员成长计划 (67)
Blogroll
gallery
Linux guru
推荐网站
Recent Comments
- Dig on 嵌入式GUI FTK设计与实现-事件源(FtkSource)
- 用心生活每一天 » GNU gprof: linux profiling tools 使用 on gcc profiling的工作原理
- JavaScript for: i++ vs i–-传播、沟通、分享-一直“有你” on i++循环与i–循环的执行效率
- Frankly Law on 嵌入式GUI FTK介绍(11)-交叉编译
- tracing on Linux下的调试工具
- ndljsn on FTK移植指南(初稿)
- tracing on 爬塘朗山
- tracing on GTK+(基于DirectFB)的字体处理
- Kely on 系统程序员成长计划写作提纲
- tracing on 爬塘朗山



November 24th, 2008
yetiboy
November 24th, 2008
画个图可能直观点:
在文件中时:
+—– +
|magic|
|No. |
+—– +
|other |
|stuff |
+—– +
|bss |
|size |
+—– +
|DATA |
| |
+—– +
|TEXT |
| |
+—– +
在进程空间中时:
+——+
|stack |
| |
+——+
|hole |
| |
+——+
|heap |
| |
+——+
|BSS |
| |
+——+
|DATA |
| |
+——+
|TEXT |
| |
+——+
|hole |
| |
+——+
yetiboy
November 25th, 2008
额,从Emacs中拷过来就变成这样的了。。。自动把空格个处理了,呵呵
Dig
November 25th, 2008
学习中。
“在x86平台上,栈顶寄存器为ESP,那么ESP的值在是PUSH操作之前修改呢,还是在PUSH操作之后修改呢?PUSH ESP这条指令会向栈中存入什么数据呢?据说x86系列CPU中,除了286外,都是先修改ESP,再压栈的。由于286没有CPUID指令,有的OS用这种方法检查286的型号”这话好像在先进之前的博文上看见过。
顺便请教下,如果用const 修饰的变量都放在.rodata中,是不是 const char * 这样的变量所占的空间就无法释放了?
Dig
November 25th, 2008
冒昧再请教一个问题,关于“以后每次写完程序都应该用valgrind跑一遍“的。写过demo,使用glib 的g_list_创建一个链表,随便加点东西,再释放,最后g_list_free,就会报告又内存泄漏,请问是为什么呢?
#include nbsp;
void
free_list_node nbsp;(gpointer nbsp;data, nbsp;gpointer nbsp;user_data)
{
nbsp; nbsp;if nbsp;(data nbsp;!= nbsp;NULL)
nbsp; nbsp; nbsp; nbsp;{
nbsp; nbsp; nbsp; nbsp; nbsp; nbsp;g_free nbsp;(data);
nbsp; nbsp; nbsp; nbsp;}
}
void
print_list_node nbsp;(gpointer nbsp;data, nbsp;gpointer nbsp;user_data)
{
nbsp; nbsp;gchar nbsp;*str nbsp;= nbsp;(gchar nbsp;*) nbsp;data;
nbsp; nbsp;g_print nbsp;(“%s\n”, nbsp;str);
}
int
main nbsp;(int nbsp;argc, nbsp;char nbsp;**argv)
{
nbsp; nbsp;GList nbsp;*list nbsp;= nbsp;NULL;
nbsp; nbsp;list nbsp;= nbsp;g_list_append nbsp;(list, nbsp;g_strdup nbsp;(“abc”));
nbsp; nbsp;list nbsp;= nbsp;g_list_append nbsp;(list, nbsp;g_strdup nbsp;(“def”));
nbsp; nbsp;list nbsp;= nbsp;g_list_append nbsp;(list, nbsp;g_strdup nbsp;(“ghi”));
nbsp; nbsp;g_list_foreach nbsp;(list, nbsp;print_list_node, nbsp;NULL);
nbsp; nbsp;g_list_foreach nbsp;(list, nbsp;free_list_node, nbsp;NULL);
nbsp; nbsp;g_list_free nbsp;(list);
nbsp; nbsp;return nbsp;0;
}
#valgrind –leak-check=full ./g_list
==9432== LEAK SUMMARY:
==9432== definitely lost: 0 bytes in 0 blocks.
==9432== possibly lost: 744 bytes in 3 blocks.
Dig
November 25th, 2008
败了。。。
#include <glib.h>
void
free_list_node (gpointer data, gpointer user_data)
{
if (data != NULL)
{
g_free (data);
}
}
void
print_list_node (gpointer data, gpointer user_data)
{
gchar *str = (gchar *) data;
g_print (“%s\n”, str);
}
int
main (int argc, char **argv)
{
GList *list = NULL;
list = g_list_append (list, g_strdup (“abc”));
list = g_list_append (list, g_strdup (“def”));
list = g_list_append (list, g_strdup (“ghi”));
g_list_foreach (list, print_list_node, NULL);
g_list_foreach (list, free_list_node, NULL);
g_list_free (list);
return 0;
}
admin
November 25th, 2008
to yetiboy : 谢谢补充。
to dig: 跑glib的程序要禁掉它的内存管理机制,我有篇BLOG讲过(我忘记了,你找找)。 possibly lost的也可以不用管它。
lyb
March 18th, 2009
可能的泄露是gobject那套东西(类型注册、管理)引起的, 不用担心
The linux mobile development » Blog Archive » 系统程序员成长计划写作提纲
June 24th, 2009
[...] 拥抱变化(上)(下) 1.5 Don’t Repeat Yourself(DRY) (上)(下) 1.6 你的数据放在哪里(上)(下) 第2章 写得又快又好的秘诀 完成 2.1 好与快的关系(上)(下) 2.2 代码阅读法 [...]