读书-程序员的自我修养-链接、封装与库(24:第十章:内存(1)程序的内存布局,栈与调用惯例

内存是承载程序运行的介质,也是程序运行计算和表达的场所。

1. 程序的内存布局

1.1 内核空间和用户空间

现在的应用程序都运行在一个内存空间中,如32位操作系统,这个内存空间由4GB的寻址能力,可以把它简单化看成一个4GB大的数组。

其中,4GB的内存空间高1G的内存空间留给内核使用,剩下的3GB空间留给程序使用,称为用户空间。但是3GB的用户空间并不是程序全部都可以使用的。

用户空间被划分为如下几个区域:栈,堆,可执行文件映像,保留区。

1.2 Linux 进程地址空间布局

linux下一个进程的内存空间从上到下依次是:
内核空间->栈->动态库->堆->读写段数据->只读段数据->保留区域
其中,箭头表示可变区的增长方向。可见,栈向低地址增长,堆向高地址增长。
如下图所示:
10-1

1.3 段错误 segment fault

在开发中,经常出现段错误/segment fault 或者 非法操作,该内存地址不能读写等错误信息。
原因:

  1. 指针指向不可读取的内存地址时出新这个错误。
  2. 代码中常见原因:
  3. 对NULL 指针进行读写;
    开始初始化指针,但是后面忘了给它赋值就开始使用空指针
  4. 使用没有初始化的栈上的指针
    没有初始化栈上的指针,它一般是一个随机值,然后使用它。

2. 栈与调用惯例

2.1 栈的特点

  1. 先进先出 FIFO
  2. 栈向下增长
  3. 栈顶由esp寄存器进行定位,栈底由ebp寄存器定位
    这里, ebp 被称为 帧指针

2.2 堆栈帧

2.2.1 堆栈帧(活动记录)定义

栈保存了一个函数调用所需要维护的信息,这被称为堆栈帧,或者叫做活动记录。
如图所示:
10-4

2.2.2 堆栈帧内容

堆栈帧包括以下内容:

  1. 函数的返回地址和参数
  2. 临时变量
  3. 保存的上下文:包括函数前后保持不变的寄存器

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# 

程序员的自我修养书籍的反汇编结果
10-5

2.2.4 windows 下的 烫 屯 分析

在vc中使用未初始化的变量或内存的值是烫。
原因是:
debug模式下,每个字节都初始化为0xCC。
其中,两个连续的0xCCCC编码就是烫。

图10-6

2.3 栈的调用惯例

2.3.1 调用惯例定义

函数调用者和被调用者之间的约定,这个约定称为调用惯例。

2.3.2 调用惯例内容

  1. 函数参数的传递顺序和方式
    函数参数传递方式有多种,最常见就是通过栈传递。
    顺序:从左至右还是从右至左。

  2. 栈的维护方式
    栈弹出可以由调用方和被调用方均可。

  3. 名字修饰和策略
    为了链接的时候对调用惯例进行区分,所以需要对函数本身的名字进行修饰。
    不同的调用惯例有不同的修饰方式。

2.3.3 默认调用惯例: cdecl

c语言默认的调用惯例是 cdecl。
如 int _cdecl foo(int a, int b)
其中,_cdecl 是非标准关键字
cdecl调用的特点:

  1. 参数从右至左压栈
    如: b先进栈, a再进栈

  2. 函数调用方负责出栈

  3. 名字修饰:直接在函数名称前加一个下划线
    如 _foo

猜你喜欢

转载自blog.csdn.net/lqy971966/article/details/108048505