在函数调用的时候一般开始的时候是在分配栈空间,结束的时候是在回收栈空间。
1.程序典型的内存分区
高地址
+------------------+ 0x7fffffff
| 栈 | ← 局部变量、函数参数、返回地址
| ↓ |
+------------------+
| |
| (动态分配) |
| |
+------------------+
| 堆 | ← 动态分配的内存 (malloc/new)
| ↑ |
+------------------+
| BSS段 | ← 未初始化的全局/静态变量
+------------------+
| 数据段(.data) | ← 已初始化的全局/静态变量
+------------------+
| 只读数据(.rodata)| ← 字符串常量、只读变量
+------------------+
| 代码段(.text) | ← 程序指令
+------------------+ 0x00400000
低地址
2.各个分区的作用和特点
代码段 (.text)
作用和特点
代码段存储程序的可执行指令,是程序的核心部分。
主要特点:
- 只读性:代码段通常是只读的,防止程序运行时意外修改指令
- 可执行:CPU可以从这个区域取指令并执行
- 共享性:多个进程可以共享同一个程序的代码段
- 位置固定:在程序加载时确定位置,运行时不会改变
数据段 (.data)
作用和特点
数据段存储程序的已初始化的全局变量和静态变量。
主要特点:
- 可读可写:程序运行时可以修改数据段中的值
- 初始化:包含程序启动时就有确定值的变量
- 持久性:在整个程序运行期间都存在
- 固定大小:编译时就确定了大小
BSS段 (.bss)
BSS段存储未初始化的全局变量和静态变量。
特点:
- 零初始化:程序启动时自动初始化为0
- 不占文件空间:在可执行文件中不占实际空间
- 运行时分配:程序加载时才分配内存
3.为什么说栈是向下生长的呢?
因为栈底是高地址,而栈顶是低地址,sp代表的是栈指针寄存器,指向帧顶,栈顶是小地址。 与此对应的还有一个栈底,bp,bp代表的是基地址寄存器,是指向栈底的,是高地址。而正常的压栈操作,是移动sp,实际上是让sp减去一个值,然后把这块空间放某个寄存器。所以栈他是向下生长的。
4.什么是大小端?
字节序(Byte Order):指多字节数据类型(如16位、32位、64位整数)在内存中字节的排列顺序。 可以简单的说大端序是高字节的数据放在地址比较低的位置,而小端序是高字节的地址放在地址比较低的位置。
- 大端序(Big Endian):最高有效字节(MSB)存储在最低内存地址
- 小端序(Little Endian):最低有效字节(LSB)存储在最低内存地址
4.函数的分类
- 叶子函数:这个函数内部的实现没有调用其他函数
- 非叶子函数:函数内部可能还调用其他函数
叶子函数
下面是对应的汇编
从上图可以看出只要移动sp就可以开辟一块新的栈空间给非叶子函数使用。
非叶子函数
下面的函数hehe就是非叶子函数
void haha()
{
int a = 2;
int b = 3;
}
void hehe()
{
int a = 4;
int b = 5;
haha();
}
下面是对应的汇编代码
sub sp, sp, #32 ; =32
stp x29, x30, [sp, #16] ; 8-byte Folded Spill
add x29, sp, #16 ; fp = sp + 16
mov w8, #5
orr w9, wzr, #0x4
stur 4, [x29, #-4]
str 5, [sp, #8]
bl _haha
ldp x29, x30, [sp, #16] ; 8-byte Folded Reload
add sp, sp, #32 ; =32
ret
简单分析下上图:
- 首先sp 地址是在0x10021,然后sp-32,那么sp变成0x10001,
- 后面近接着将sp的地址加上16,然后先存x29,x29其实就是fp寄存器,就是栈底指针。x30就是lr寄存器,其实就是当前的执行完返回后的指令地址,这一步其实是在保护现场,是在调用函数之前把栈底指针,返回地址,先存起来。一共用了16个字节,其实就是从0X10020到0x10010之间。
- 后面再把sp 加16的地址赋值给x29,这是新的栈底位置。此时从sp到x29(fp)之间的就是当前调用函数可以使用的栈空间
- 中间的5行代码就是在执行里面赋值和跳转操作
- 最后三行 先是将 sp +16 位置开始取出16个字节的数据赋值给x29(fp),和lr(就是当前函数地址执行完后的返回地址)。然后再将sp + 32 调整sp回到开始的位置,最后返回