zoukankan      html  css  js  c++  java
  • 系统程序员成长计划你的数据放在哪里(下)

    转载时请注明出处和作者联系方式
    文章出处: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]的内存区间表示是堆。

    对内存的掌握是系统程序员必备的技能,希望读者多加体会。本节示例代码请到这里下载

  • 相关阅读:
    298. Binary Tree Longest Consecutive Sequence
    117. Populating Next Right Pointers in Each Node II
    116. Populating Next Right Pointers in Each Node
    163. Missing Ranges
    336. Palindrome Pairs
    727. Minimum Window Subsequence
    211. Add and Search Word
    年底购物狂欢,移动支付安全不容忽视
    成为程序员前需要做的10件事
    全球首推iOS应用防破解技术!
  • 原文地址:https://www.cnblogs.com/zhangyunlin/p/6167480.html
Copyright © 2011-2022 走看看