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]的内存区间表示是堆。

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

  • 相关阅读:
    Python 安装、循环语句、数据类型(一)
    Python 字符、整型、列表字典等操作(二)
    JAVA实现文章的上一篇下一篇
    什么是公钥、私钥、密码、助记词
    XSS跨站网络攻击
    区块链相关知识
    冷钱包
    .NET 后台提交POST请求
    邮件的发送
    数据库中字段类型对应的C#中的数据类型
  • 原文地址:https://www.cnblogs.com/zhangyunlin/p/6167480.html
Copyright © 2011-2022 走看看