读书-程序员的自我修养-链接、封装与库(24:第十章:内存(1)程序的内存布局,栈与调用惯例)
内存是承载程序运行的介质,也是程序运行计算和表达的场所。
1. 程序的内存布局
1.1 内核空间和用户空间
现在的应用程序都运行在一个内存空间中,如32位操作系统,这个内存空间由4GB的寻址能力,可以把它简单化看成一个4GB大的数组。
其中,4GB的内存空间高1G的内存空间留给内核使用,剩下的3GB空间留给程序使用,称为用户空间。但是3GB的用户空间并不是程序全部都可以使用的。
用户空间被划分为如下几个区域:栈,堆,可执行文件映像,保留区。
1.2 Linux 进程地址空间布局
linux下一个进程的内存空间从上到下依次是:
内核空间->栈->动态库->堆->读写段数据->只读段数据->保留区域
其中,箭头表示可变区的增长方向。可见,栈向低地址增长,堆向高地址增长。
如下图所示:
1.3 段错误 segment fault
在开发中,经常出现段错误/segment fault 或者 非法操作,该内存地址不能读写等错误信息。
原因:
- 指针指向不可读取的内存地址时出新这个错误。
- 代码中常见原因:
- 对NULL 指针进行读写;
开始初始化指针,但是后面忘了给它赋值就开始使用空指针 - 使用没有初始化的栈上的指针
没有初始化栈上的指针,它一般是一个随机值,然后使用它。
2. 栈与调用惯例
2.1 栈的特点
- 先进先出 FIFO
- 栈向下增长
- 栈顶由esp寄存器进行定位,栈底由ebp寄存器定位
这里, ebp 被称为 帧指针
2.2 堆栈帧
2.2.1 堆栈帧(活动记录)定义
栈保存了一个函数调用所需要维护的信息,这被称为堆栈帧,或者叫做活动记录。
如图所示:
2.2.2 堆栈帧内容
堆栈帧包括以下内容:
- 函数的返回地址和参数
- 临时变量
- 保存的上下文:包括函数前后保持不变的寄存器
2.2.3 foo.o 的反汇编
root@ubuntu-admin-a1:/home/6Chapter# cat foo.c
int foo()
{
return 123;
}
root@ubuntu-admin-a1:/home/6Chapter# objdump -d foo.o
foo.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <foo>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: b8 7b 00 00 00 mov $0x7b,%eax
9: 5d pop %rbp
a: c3 retq
root@ubuntu-admin-a1:/home/6Chapter#
程序员的自我修养书籍的反汇编结果
2.2.4 windows 下的 烫 屯 分析
在vc中使用未初始化的变量或内存的值是烫。
原因是:
debug模式下,每个字节都初始化为0xCC。
其中,两个连续的0xCCCC编码就是烫。
2.3 栈的调用惯例
2.3.1 调用惯例定义
函数调用者和被调用者之间的约定,这个约定称为调用惯例。
2.3.2 调用惯例内容
-
函数参数的传递顺序和方式
函数参数传递方式有多种,最常见就是通过栈传递。
顺序:从左至右还是从右至左。 -
栈的维护方式
栈弹出可以由调用方和被调用方均可。 -
名字修饰和策略
为了链接的时候对调用惯例进行区分,所以需要对函数本身的名字进行修饰。
不同的调用惯例有不同的修饰方式。
2.3.3 默认调用惯例: cdecl
c语言默认的调用惯例是 cdecl。
如 int _cdecl foo(int a, int b)
其中,_cdecl 是非标准关键字
cdecl调用的特点:
-
参数从右至左压栈
如: b先进栈, a再进栈 -
函数调用方负责出栈
-
名字修饰:直接在函数名称前加一个下划线
如 _foo