Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1711983
  • 博文数量: 143
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 1462
  • 用 户 组: 普通用户
  • 注册时间: 2016-08-23 11:14
文章分类

全部博文(143)

文章存档

2022年(3)

2021年(13)

2020年(21)

2019年(8)

2018年(28)

2017年(7)

2016年(63)

我的朋友

分类: 嵌入式

2018-10-02 11:03:52

简介:本文想描写一下进程的内存布局,注意这里的内存是指虚拟地址空间。由于作者wilcohuang已经转载并总结的很好了,正如作者wilcohuang所说:前人已经总结写的很好了,纵使再花大量时间也无法达到这个水平。所以,本文仅仅是在转载的基础上,重新排版和标红加粗,方便理解记忆。
原文:Linux虚拟地址空间布局以及进程栈和线程栈总结 作者:wilcohuang

一、Linux虚拟地址空间布局

在多任务操作系统中,每个进程都运行在属于自己的内存沙盘中。这个沙盘就是虚拟地址空间(Virtual Address Space),在32位模式下它是一个4GB的内存地址块。在Linux系统中, 内核进程和用户进程所占的虚拟内存比例是1:3,而Windows系统为2:2(通过设置Large-Address-Aware Executables标志也可为1:3)。这并不意味着内核使用那么多物理内存,仅表示它可支配这部分地址空间,根据需要将其映射到物理内存。实际上,Linux运行在虚拟地址空间,并负责把系统中实际存在的远小于4GB的物理地址空间(物理内存)根据不同需求映射到整个4GB的虚拟地址空间中。这就说同一块物理内存可能映射到多处虚拟内存地址空间上,这正是Linux内存管理职责所在。

虚拟地址通过页表(Page Table)映射到物理内存,页表由操作系统维护并被处理器引用。内核空间在页表中拥有较高特权级,因此用户态程序试图访问这些页时会导致一个页错误(page fault)。在Linux中,内核空间是持续存在的,并且在所有进程中都映射到同样的物理内存。内核代码和数据总是可寻址,随时准备处理中断和系统调用。与此相反,用户模式地址空间的映射随进程切换的发生而不断变化。

Linux进程在虚拟内存中的标准内存段布局如下图所示:

其中,用户地址空间中的蓝色条带对应于映射到物理内存的不同内存段,灰白区域表示未映射的部分。这些段只是简单的内存地址范围,与Intel处理器的段没有关系。

上图中Random stack offsetRandom mmap offset等随机值意在防止恶意程序。Linux通过对栈、内存映射段、堆的起始地址加上随机偏移量来打乱布局,以免恶意程序通过计算访问栈、库函数等地址。execve(2)负责为进程代码段和数据段建立映射,真正将代码段和数据段的内容读入内存是由系统的缺页异常处理程序按需完成的。另外,execve(2)还会将BSS段清零。

32位模式下进程内存经典布局(Linux kernel 2.6.7以前)

这种布局是 Linux 内核 2.6.7 以前的默认进程内存布局形式,mmap 区域与栈区域相对增 长,这意味着堆只有 1GB 的虚拟地址空间可以使用,继续增长就会进入 mmap 映射区域, 这显然不是我们想要的。这是由于 32 模式地址空间限制造成的,所以内核引入了另一种虚 拟地址空间的布局形式,将在后面介绍。但对于 64 位系统,提供了巨大的虚拟地址空间,这种布局就相当好。

32位模式下进程内存默认布局(Linux kernel 2.6.7以后)


从上图可以看到,栈至顶向下扩展,并且栈是有界的。堆至底向上扩展,mmap 映射区 域至顶向下扩展,mmap 映射区域和堆相对扩展,直至耗尽虚拟地址空间中的剩余区域,这 种结构便于 C 运行时库使用 mmap 映射区域和堆进行内存分配。上图的布局形式是在内核 2.6.7 以后才引入的,这是 32 位模式下进程的默认内存布局形式。

64位模式下进程内存经典布局

64 位模式下各个区域的起始位置是什么呢?对于 AMD64 系统,内存布局采用经典 内存布局,text 的起始地址为 0x0000000000400000,堆紧接着 BSS 段向上增长,mmap 映射 区域开始位置一般设为 TASK_SIZE/3

点击(此处)折叠或打开

  1. #define TASK_SIZE_MAX ((1UL << 47) - PAGE_SIZE)
  2. #define TASK_SIZE (test_thread_flag(TIF_IA32) ? IA32_PAGE_OFFSET : TASK_SIZE_MAX)
  3. #define STACK_TOP TASK_SIZE
  4. #define TASK_UNMAPPED_BASE (PAGE_ALIGN(TASK_SIZE / 3))

计算一下可知,mmap 的开始区域地址为 0x00002AAAAAAAA000,栈顶地址为 0x00007FFFFFFFF000

上图是 X86_64 Linux 进程的默认内存布局形式,这只是一个示意图,当前内核默认 配置下,进程的栈和 mmap 映射区域并不是从一个固定地址开始,并且每次启动时的值都 不一样,这是程序在启动时随机改变这些值的设置,使得使用缓冲区溢出进行攻击更加困难。当然也可以让进程的栈和 mmap 映射区域从一个固定位置开始,只需要设置全局变量 randomize_va_space 值为 0 , 这 个 变 量 默 认 值 为 1 。 用 户 可 以 通 过 设 置 /proc/sys/kernel/randomize_va_space 来停用该特性,也可以用如下命令:

点击(此处)折叠或打开

  1. sudo sysctl -w kernel.randomize_va_space=0

另外,额外奉送很早前我自己总结并画的32位模式下进程内存布局(弃之可惜):



QA:Linux中所有的应用程序都是这个布局,每个应用程序都是从0x80480000这个地址开始的,这样冲突吗?

AN:

不冲突,因为这个地址是一个虚拟地址,Linux中每个应用程序都有自己的虚拟地址空间,而这些虚拟地址映射到物理内存的不同内存段

二、Linux用户进程内存布局

Linux用户进程分段存储内容

Section

属性

存储内容

 

局部变量、const局部常量、函数参数、返回地址等

 

动态分配的内存

BSS

可读;可写

未初始化/初始化为0的静态变量/全局变量

数据段

可读;可写

初始化为~0的静态变量/全局变量

代码段

只读;可执行

可执行代码、常量(字符串常量;const全局常量;enum常量;#define常量等)

在将应用程序加载到内存空间执行时,操作系统负责代码段、数据段和BSS段的加载,并在内存中为这些段分配空间。栈也由操作系统分配和管理;堆由程序员自己管理,即显式地申请和释放空间。

BSS段、数据段和代码段是可执行程序编译时的分段,运行时还需要栈和堆。

1.内核空间

内核总是驻留在内存中,是操作系统的一部分。内核空间为内核保留,不允许应用程序读写该区域的内容或直接调用内核代码定义的函数。

2.栈

进程栈的初始化大小是由编译器和链接器计算出来的,但是栈的实时大小并不是固定的,程序在运行时Linux内核会根据入栈情况对栈区进行动态增长(其实也就是添加新的页表)。

但是并不是说栈区可以无限增长,它也有最大限制RLIMIT_STACK(一般为 8M---在32位模式下的进程内存默认布局(Linux kernel 2.6.7以后)中),在Linux中可以通过ulimit -s命令查看和设置栈的最大值,当程序使用的栈超过该值时, 发生栈溢出(Stack Overflow),程序收到一个段错误(Segmentation Fault)。注意,调高栈容量可能会增加内存开销和启动时间。

栈既可向下增长(向内存低地址)也可向上增长, 这依赖于具体的实现。本文所述栈向下增长。

3.内存映射段(mmap)

mmap其实和堆一样,也是动态分配内存的。

Linux中,若通过malloc()请求一大块内存,C运行库将创建一个匿名内存映射(该映射没有对应的文件, 可用于存放程序数据),而不使用堆内存。”大块” 意味着比阈值 MMAP_THRESHOLD还大,缺省为128KB,可通过mallopt()调整。

该区域用于映射可执行文件用到的动态链接库。在Linux 2.4版本中,若可执行文件依赖共享库,则系统会为这些动态库在从0x40000000开始的地址分配相应空间,并在程序装载时将其载入到该空间。在Linux 2.6内核中,共享库的起始地址被往上移动至更靠近栈区的位置。

从进程地址空间的布局可以看到,在有共享库的情况下,留给堆的可用空间还有两处:一处是从.bss段到0x40000000,约不到1GB的空间;另一处是从共享库到栈之间的空间,约不到2GB。这两块空间大小取决于栈、共享库的大小和数量。这样来看,是否应用程序可申请的最大堆空间只有2GB事实上,这与Linux内核版本有关。在上面给出的进程地址空间经典布局图中,共享库的装载地址为0x40000000,这实际上是Linux kernel 2.6版本之前的情况了,在2.6版本里,共享库的装载地址已经被挪到靠近栈的位置,即位于0xBFxxxxxx附近,因此,此时的堆范围就不会被共享库分割成2个“碎片”,kernel 2.632Linux系统中,malloc申请的最大内存理论值在2.9GB左右。

4.堆(heap)

分配的堆内存是经过字节对齐的空间,以适合原子操作。堆管理器通过链表管理每个申请的内存,由于堆申请和释放是无序的,最终会产生内存碎片。堆内存一般由应用程序分配释放,回收的内存可供重新使用。若程序员不释放,程序结束时操作系统可能会自动回收。

堆的末端由break指针标识,当堆管理器需要更多内存时,可通过系统调用brk()sbrk()来移动break指针以扩张堆,一般由系统自动调用。

使用堆时经常出现两种问题:1) 释放或改写仍在使用的内存(“内存破坏”)2)未释放不再使用的内存(“内存泄漏”)。当释放次数少于申请次数时,可能已造成内存泄漏。泄漏的内存往往比忘记释放的数据结构更大,因为所分配的内存通常会圆整为下个大于申请数量的2的幂次(如申请212B,会圆整为256B)

5.BSS段

BSS段在目标文件中并不占据空间,这样可减少目标文件体积。但程序运行时需为变量分配内存空间,故目标文件必须记录所有未初始化的静态分配变量大小总和(通过start_bssend_bss地址写入机器代码)。当加载器(loader)加载程序时,将为BSS段分配的内存初始化为0。在嵌入式软件中,进入main()函数之前BSS段被C运行时系统映射到初始化为全零的内存(效率较高)

注意,尽管均放置于BSS段,但初值为0的全局变量是强符号,而未初始化的全局变量是弱符号。若其他地方已定义同名的强符号(初值可能非0),则弱符号与之链接时不会引起重定义错误,但运行时的初值可能并非期望值(会被强符号覆盖)。因此,定义全局变量时,若只有本文件使用,则尽量使用static关键字修饰;否则需要为全局变量定义赋初值(哪怕0),保证该变量为强符号,以便链接时发现变量名冲突,而不是被未知值覆盖。

某些编译器将未初始化的全局变量保存在common段,链接时再将其放入BSS段。在编译阶段可通过-fno-common选项来禁止将未初始化的全局变量放入common段。

此外,由于目标文件不含BSS段,故程序烧入存储器(Flash)BSS段地址空间内容未知。U-Boot启动过程中,将U-BootStage2代码(通常位于lib_xxxx/board.c文件)搬迁(拷贝)SDRAM空间后必须人为添加清零BSS段的代码,而不可依赖于Stage2代码中变量定义时赋0值。

点击(此处)折叠或打开

  1. 【扩展阅读】BSS历史
  2. BSS(Block Started by Symbol,以符号开始的块)一词最初是UA-SAP汇编器(United Aircraft Symbolic Assembly Program)中的伪指令,用于为符号预留一块内存空间。该汇编器由美国联合航空公司于20世纪50年代中期为IBM 704大型机所开发。
  3. 后来该词被作为关键字引入到了IBM 709和7090/94机型上的标准汇编器FAP(Fortran Assembly Program),用于定义符号并且为该符号预留指定字数的未初始化空间块。
  4. 在采用段式内存管理的架构中(如Intel 80x86系统),BSS段通常指用来存放程序中未初始化全局变量的一块内存区域,该段变量只有名称和大小却没有值。程序开始时由系统初始化清零。
  5. BSS段不包含数据,仅维护开始和结束地址,以便内存能在运行时被有效地清零。BSS所需的运行时空间由目标文件记录,但BSS并不占用目标文件内的实际空间,即BSS节段应用程序的二进制映象文件中并不存在。

6.数据段(Data)

数据段保存在目标文件中(在嵌入式系统里一般固化在镜像文件中),其内容由程序初始化。例如,对于全局变量int gVar = 10,必须在目标文件数据段中保存10这个数据,然后在程序加载时复制到相应的内存。

当程序读取数据段的数据时,系统会触发缺页故障,从而分配相应的物理内存;当程序读取BSS段的数据时,内核会将其转到一个全零页面,不会发生缺页故障,也不会为其分配相应的物理内存。

7.代码段(text)

代码段指令中包括操作码和操作对象(或对象地址引用)。若操作对象是立即数(具体数值),将直接包含在代码中;若是局部数据,将在栈区分配空间,然后引用该数据地址;若位于BSS段和数据段,同样引用该数据地址。

8.保留区

它并不是一个单一的内存区域,而是对地址空间中受到操作系统保护而禁止用户进程访问的地址区域的总称。大多数操作系统中,极小的地址通常都是不允许访问的,如NULLC语言将无效指针赋值为0也是出于这种考虑,因为0地址上正常情况下不会存放有效的可访问数据。

32X86架构的Linux系统中,用户进程可执行程序一般从虚拟地址空间0x08048000开始加载。该加载地址由ELF文件头决定,可通过自定义链接器脚本覆盖链接器默认配置,进而修改加载地址。0x08048000以下的地址空间通常由C动态链接库、动态加载器ld.so和内核VDSO(内核提供的虚拟共享库)等占用。通过使用mmap系统调用,可访问0x08048000以下的地址空间。

QA: 栈顶和堆头地址是多少?

AN:

见上文中图。

QA:栈和堆的最大容量是多少?

AN:

要注意:①关于栈堆的最大容量是多少的问题,其实,在Linux kernel 2.6.7版本以后,更明确的问法是:程栈大小规定是多少,及如何查看和修改?理论上堆大小是多少?编译期限制栈大小,和系统限制栈大小根本是两回事。系统限制栈大小是限制进程主线程的栈大小,限制的是整个函数调用链的最大栈大小,而这个栈大小是函数调用链上各个函数栈帧大小之和。编译期限制栈大小是限制单个函数栈帧的大小。

见上文中标红。

QA:栈和堆的虚拟地址空间是连续的嘛?

AN:

见下文中标红。

QA:栈和堆的区别?

AN:

①管理方式:栈由编译器自动管理;堆由程序员控制,使用方便,但易产生内存泄露。②生长方向:栈向低地址扩展(即”向下生长”),是连续的内存区域;堆向高地址扩展(即”向上生长”),是不连续的内存区域。这是由于系统用链表来存储空闲内存地址,自然不连续,而链表从低地址向高地址遍历。③空间大小:栈顶地址和栈的最大容量由系统预先规定(通常默认8M---在32位模式下的进程内存默认布局(Linux kernel 2.6.7以后)中);堆的大小则受限于计算机系统中有效的虚拟内存(32Linux系统中堆内存可达2.9G空间)。④存储内容:栈在函数调用时,首先压入主调函数中下条指令(函数调用语句的下条可执行语句)的地址,然后是函数实参,然后是被调函数的局部变量。本次调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的指令地址,程序由该点继续运行下条可执行语句。堆通常在头部用一个字节存放其大小,堆用于存储生存期与函数调用无关的数据,具体内容由程序员安排。⑤分配方式:栈可静态分配或动态分配。静态分配由编译器完成,如局部变量的分配。动态分配由alloca函数在栈上申请空间,用完后自动释放。堆只能动态分配且手工释放。⑥分配效率:栈由计算机底层提供支持:分配专门的寄存器存放栈地址,压栈出栈由专门的指令执行,因此效率较高。堆由函数库提供,机制复杂,效率比栈低得多。Windows系统中VirtualAlloc可直接在进程地址空间中分配一块内存,快速且灵活。⑦碎片问题:栈不会存在碎片问题,因为栈是先进后出的队列,内存块弹出栈之前,在其上面的后进的栈内容已弹出。而频繁申请释放操作会造成堆内存空间的不连续,从而造成大量碎片,使程序效率降低。

QA:进程的堆栈和数据结构的堆栈的区别?

AN:

进程的栈:(我把它称为栈区、进程栈)

栈是指程序运行时存放的参数、局部变量、函数返回地址等,由操作系统自动分配和释放,栈区又称堆栈区。由于栈是连续的内存区域,所以栈是一种线性结构。其行为(操作方式)类似数据结构中的栈

进程的堆:(我把它称为堆区、进程堆)

堆是指程序运行时申请的动态内存,由程序员自己分配和释放。由于堆是不连续的内存区域,所以系统用链式结构来存储空闲内存地址。注意,进程堆不同于数据结构中的堆,其行为(操作方式)类似链表

数据结构中的栈:(我把它称为栈结构)

栈是一种后进先出LIFO的线性表,在栈顶插入和删除元素。栈有两种存储结构来实现:顺序栈、链栈。

数据结构中的堆:(我把它称为堆结构)

堆也被称为优先队列,尽管名为优先队列,但堆并不是队列,而其实际实现是一个完全二叉树

先看看什么是队列?队列是一种先进先出FIFO的线性表,在队尾插入元素,在队头取出元素。而优先队列也是一样的,但是在优先队列中,元素被赋予优先级,即我们不是按照元素进入队列的先后顺序取出元素的,而是按照元素的优先级取出元素,这个优先级可以是元素的大小或者其他规则。优先队列按数据结构的不同有几种实现方式:有序数组、无序数组、完全二叉树,而使用完全二叉树来实现的优先队列在时间效率上是最高的。

同样的,我们也可以用完全二叉树来实现堆排序

FR:    纸上谈兵: (heap)


三、Linux目标文件的bss段、数据段、代码段

下面我们通过如下程序来进一步说明C程序中各数据的内存布局:

点击(此处)折叠或打开

  1. #include <stdio.h>
  2. #include <stdlib.h>

  3. int A;                    //全局未初始化变量
  4. int B = 0;                //全局初始化为0的变量
  5. int C = 2;                //全局初始化变量
  6. int static D;                //全局静态未初始化变量
  7. int static E = 0;            //全局静态初始化为0的变量
  8. int static F = 4;            //全局静态初始化变量
  9. int const G = 5;            //全局常量

  10. int main()
  11. {
  12.     int a;                //局部未初始化变量
  13.     int b = 0;                //局部初始化为0的变量
  14.     int c = 2;                //局部初始化变量
  15.     int static d;            //局部静态未初始化变量
  16.     int static e = 0;            //局部静态初始化为0的变量
  17.     int static f = 4;            //局部静态初始化变量
  18.     int const g = 5;            //局部常量

  19.     char h1[] = "abcde";        //局部字符数组
  20.     char* h2 = "abcde";            //局部指针指向字符串常量

  21.     int* i = malloc(sizeof(int));    //

  22.     printf("A_global_addr = %p\n",&A);
  23.     printf("B_global_init0_addr = %p\n",&B);
  24.     printf("C_global_init_addr = %p\n",&C);
  25.     printf("D_global_static_addr = %p\n",&D);
  26.     printf("E_global_static_init0_addr = %p\n",&E);
  27.     printf("F_global_static_init_addr = %p\n",&F);
  28.     printf("G_global_const_addr = %p\n",&G);

  29.     printf("\n");

  30.     printf("a_addr = %p\n",&a);
  31.     printf("b_init0_addr = %p\n",&b);
  32.     printf("c_init_addr = %p\n",&c);
  33.     printf("d_static_addr = %p\n",&d);
  34.     printf("e_static_init0_addr = %p\n",&e);
  35.     printf("f_static_init_addr = %p\n",&f);
  36.     printf("g_const_addr = %p\n",&g);

  37.     printf("\n");

  38.     printf("h1_arr_addr = %p\n",h1);
  39.     printf("h2_strconst_addr = %p\n",h2);
  40.     printf("h2_point_addr = %p\n",&h2);
  41.     printf("i_malloc_addr = %p\n",i);

  42.     while(1);

  43.     free(i);

  44.     return 0;
  45. }

查看该应用程序的执行结果:

通过readlef -S a.outobjdump -h a.out查看该应用程序ELF文件的段表:


我们可以看出每个section的大小、文件偏移、虚拟地址(Virtual Memory Address)、装载地址(Load Memory Address)。需要说明的是编译生成的目标文件(.o)中,并没有分配虚拟空间地址,即VMALMA两个字段的值全为0

通过readelf -s a.outobjdump -t a.out查看该应用程序ELF文件的符号表:


通过cat /proc/pid/maps查看该应用程序的虚拟内存分布:


综上后,我们把执行结果截图依次与符号表截图(符号表截图太长,自己可以变通下,如通过objdump -t a.out | grep data来查看data段的各个数据)、段表截图、maps截图做对比:(注:符号表中是各个变量的地址和大小;段表中的是段的地址和大小;maps中的是栈和堆的地址和大小)


小结:

不同的局部变量保存在栈,动态申请的局部变量保存在堆

初始化为非0的全局变量&静态全局变量&静态局部变量保存在.data段,未初始化及初始化为0的全局变量&静态全局变量&静态局部变量保存在.bss

全局常量保存在.rodata段,而局部常量保存在栈

字符串常量保存在.rodata段,但字符串指针保存在栈,字符数组也保存在栈

本节参考:linux--进程在内存中的布局 linux应用程序内存布局 C语言的代码内存布局详解

QA:.bss段不占据ELF目标文件的空间,该如何理解?

AN:

.bss段的变量不占用ELF目标文件的空间,仅保存了.bss段的起始地址、大小、变量符号。那么.bss段的起始地址、大小、变量符号保存在哪儿呢?

ELF目标文件的段表(Section Header Table)中存放有.bss段的起始地址和大小,可以通过readlef -S a.outobjdump -h a.out查看。ELF目标文件的符号表(Symbol Table,即.symtab段)中存放有.bss段的变量符号,可以通过readelf -s a.outobjdump -t a.out查看。

总而言之,.bss段不占据ELF目标文件的空间,即不占据实际的磁盘空间,只在段表(Section Header Table)中记录起始地址和大小,在符号表(.symtab段)中记录变量符号,仅当ELF目标文件加载运行时,才分配空间以及初始化

FR: bss段不占据磁盘空间的理解 LINUX下目标文件的BSS段、数据段、代码段

QA: .data段和.bss段有什么区别

AN:

就拿全局变量来比较:bss段的全局变量只占运行时的内存空间,而不占文件空间;而data段的全局变量既占文件空间,又占用运行时的内存空间。

QA: 普通局部变量和static局部变量有什么区别

AN:

二者存储方式不同,普通局部变量保存在栈或堆上,static局部变量保存在.data段或.bss段。

二者生命周期不同,正是由于存储方式的不同,使得static局部变量在程序的整个生命周期中存在,也就是说:普通局部变量每次进入函数都要初始化一次,static局部变量只被初始化一次,即第一次进入函数时初始化,以后每次进入函数都依据上一次的结果值。但是,虽然static局部变量在函数调用结束后仍然存在,但其他函数是不能直接引用它的。

QA: 普通全局变量和static全局变量有什么区别

AN:

二者存储方式相同。

二者作用域不同,static全局变量的作用域是仅本文件有效。当一个源程序由多个源文件组成时,普通全局变量在各个源文件中都是有效的,而static全局变量则限制了其作用域, 即只在定义该static全局变量的源文件内有效, 在同一源程序的其它源文件中不能使用它。

QA: 普通函数和static函数有什么区别

AN:

二者作用域不同,static函数的作用域是仅本文件有效。一般,对于只在当前源文件中使用的函数,应该在当前源文件中声明和定义为static函数,而对于可在当前源文件以外使用的函数,应该在一个同文件中声明,要使用这些函数的源文件中包含此头文件就可以调用此函数。

QA: const的特点

AN:

const即常量,常量也就是说不能改变,保护了被修饰的东西,增强了程序的健壮性。另外需要注意的是,const定义常量时必须初始化。除此之外声明一个引用(即变量的别名)时,也必须要初始化。

const的用法?

定义常量。const定义的全局变量与#define相比,①全局常量在程序运行过程中只有一份拷贝,而宏定义常量在内存中有若干个拷贝;②全局常量有数据类型,所以编译器会对其做类型检查,而宏定义常量没有数据类型,只是做简单的字符替换,所以没有做类型检查。

定义函数入参。使编译器做类型检查,const入参不能在函数内做修改,否则编译器会报错。

定义函数。函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加const修饰的同类型指针。

QA: violate

AN:

violatile关键字通常用来修饰多线程共享的全局变量和IO内存。告诉编译器,不要把此类变量优化到寄存器中,每次都要老老实实的从内存中读取,因为它们随时都可能变化。举个简单的例子:(网上多得是,自己搜吧)

QA: #define和inline函数有什么差别?

AN:

#define不是函数,只是在编译前(编译预处理阶段)做简单的字符替换,且不做类型检查

inline函数是函数,但在编译时不单独产生代码,而是将代码直接镶嵌到调用处,减少了普通函数调用时的资源消耗,且inline函数因为是函数所以要做参数类型检查。一般的,当一个简单函数(函数内不包含for、while、switch语句)被多次调用时,就应该考虑用inline。

二者关系:直到inline操作符变为标准c的一部分,#define都是产生嵌入代码的唯一方法。两者都是以消耗空间为代价,提升了嵌入式系统的性能

QA: #define和typedef有什么关系?

AN:

点击(此处)折叠或打开

  1. #include <stdio.h>

  2. #define PUINT32_1 unsigned int *
  3. typedef unsigned int * PUINT32_2;


  4. int main()
  5. {
  6.     PUINT32_1 A,B;
  7.     PUINT32_2 a,b;

  8.     printf("%d %d %d %d\n", sizeof(A), sizeof(B), sizeof(a), sizeof(b));
  9.     return 0;
  10. }

结果:8 4 8 8

QA: sizeof和strlen有什么区别?

AN:

①sizeof是操作符;而strlen是函数。

②大部分编译程序在编译的时候就把sizeof计算过了;而strlen的结果要在运行的时候才能计算出来。

③sizeof计算类型或者变量的长度;而strlen用来计算字符串的长度。

一般的,当sizeof计算结构体大小和联合体大小时,要考虑对齐问题?

结构体:当结构体内的元素长度都小于处理器的位数时,以结构体里最长的数据元素为对齐单位,也就是说结构体的长度一定是最长数据元素的整数倍;如果结构体内存在长度大于处理器位数的元素时,就以处理器的位数为对齐单位。

联合体:首先要满足大小足够容纳最宽的成员;其次要满足大小能够被其包含的所有基本数据类型的大小所整除。


四、Linux可执行目标文件的执行过程


编译链接生成的可执行文件中已经生成了虚拟地址,就是进程执行过程中的加载和使用的地址。那么进程的执行最终还是需要加载到物理内存上,所以运行过程中最重要的逻辑就是虚拟地址到物理地址的映射了。

如下是ELF可执行目标文件的执行过程:

创建进程的虚拟地址空间

这个过程只是创建一个最重要的页表的数据结构,用于将虚拟地址空间映射到物理地址空间。 页表,每个进程都有一个页表项,进程页表用于逻辑页对应的物理页的映射。这里忽略操作系统如何分页,已经如何加载文件的过程。进程加载时会根据条件选择加载一些页到内存,当访问的页通过页表发现该页不在内存中,则发生缺页终端,进行页的加载。


②读取ELF文件头,建立虚拟空间和可执行文件的映射关系


③将寄存器设置为程序启动的入口地址,启动执行
本节选自:进程中的地址是从何而来 

附件:
glibc内存管理ptmalloc源代码分析.pdf

阅读(49146) | 评论(0) | 转发(1) |
给主人留下些什么吧!~~