> 技术文档 > 栈空间及寄存器与内存地址寻址分析

栈空间及寄存器与内存地址寻址分析


文章目录

  • 一、 寄存器
    • 1.1 CPU核心寄存器
    • 1.2 SRAM空间布局
    • 1.3 关于内存地址的划分
  • 二、单片机寻址方式

一、 寄存器

ARM单片机的核心寄存器是CPU内部的关键存储单元,CPU内部的物理电路(由触发器构成)。直接参与指令执行、数据运算和系统控制。

寄存器虽无地址,但可存储数据。

1.1 CPU核心寄存器

  • R0-R12:标准通用寄存器,其中:

  • R0-R3:函数参数传递和返回值存储(如函数返回值存于R0)。

  • R4-R11:保存局部变量,部分模式下(如Thumb)限制使用R4-R7。

以上只是举个例子,具体更多的CPU核心寄存器不做过多解释。

特殊功能寄存器​:

  • R13 (SP)​​:堆栈指针,管理函数调用时的栈空间(不同模式有独立副本,如SP_irqSP_svc)。存储栈顶的内存地址值​(如SP=0x2000表示栈从内存地址0x2000开始)。SP的核心作用​:作为栈顶地址的指针,SP存储的值必须是内存地址​(而非数据),否则CPU无法定位堆栈位置。SP(Stack Pointer)作为CPU核心寄存器不占用内存地址,它是CPU内部的专用存储单元(类似R0-R7),由触发器电路构成。 ​​“存储堆栈栈顶的16位物理地址值”​​(在8051中为8位地址)。
复位后SP值=0x07 → 堆栈起始地址=0x08 设置SP=0xBF → 堆栈起始地址=0xC0 

关于这里为什么要自增1?

堆栈指针始终指向下一次压栈操作时数据将被存储的位置​(即栈顶的空闲位置)。例如:

  • SP = 0x07,表示下一次压栈操作会从地址 0x07 开始存储数据。

  • 但此时堆栈的起始地址​(即第一个可用存储位置)实际是 SP + 1 = 0x08,因为 0x07 是即将被写入的位置,尚未存放有效数据。

场景 SP设置值 堆栈起始地址 原因 复位后 0x07 0x08 SP指向下一次压栈位置(0x07),起始地址为 SP+1=0x08。 设置 SP=0xBF 0xBF 0xC0 SP指向下一次压栈位置(0xBF),起始地址为 SP+1=0xC0

SP指向下一次压栈的操作位置,而堆栈起始地址是 SP + 1。这是由堆栈的预递减机制和硬件设计共同决定的,目的是确保首次压栈时数据精确存入预设的起始地址。

特殊功能寄存器​:

  • R14 (LR)​​:链接寄存器,保存子程序或中断返回地址(如BL指令自动更新LR)。

  • R15 (PC)​​:程序计数器,存储下一条指令地址(ARM状态下PC=当前指令地址+8)。存储下一条指令的内存地址​(如PC=0x1000表示CPU将执行0x1000处的指令)。

等其他的核心寄存器,这些寄存器是没有内存地址的,而是通过指令操作码和逻辑编号在CPU内部直接寻址。(如R0编号0、SP编号13)在指令中直接引用。

如:通过指令进行相关操作。

MOV R0, #12 ; R0 = 12(立即数加载)MOV R1, R0 ; R1 = R0(寄存器间传递)[1,4](@ref)ADD R2, R0, R1 ; R2 = R0 + R1(如12+21=33)SUB R3, R1, #5 ; R3 = R1 - 5[1,2](@ref)

MOV EAX, 1 为例,CPU 需经历以下阶段:

  1. 取指(Fetch)​

    • 程序计数器(PC)指向指令地址,从内存中读取机器码 B8 01 00 00 00 到指令寄存器(IR)。

    • PC 自动增加,指向下一条指令。

  2. 译码(Decode)​

    • 提取操作码​:CPU 解析 IR 的前 8 位(B8),识别为 MOV 指令。

    • 解析操作数​:操作码 B8 隐含目标寄存器为 EAX,后续 4 字节(01 00 00 00)是立即数 1

    • 译码器生成控制信号,激活数据传输电路。

  3. 执行(Execute)​

    • 控制单元将立即数 1 送入 EAX 寄存器的输入通路,完成数据加载。
  4. 写回(Writeback)​

    • 结果写回 EAX 寄存器,更新其值为 1
LED_BASE DCD 0x40021000 ; 定义GPIO基址Main LDR R0, =LED_BASE ; R0存储外设基址 LDR R1, [R0, #0x04] ; 读取配置寄存器(R1为中间值) ORR R1, R1, #0x03 ; 设置PA0为输出(位或操作) STR R1, [R0, #0x04] ; 写回配置寄存器[2](@ref)Loop LDR R1, [R0, #0x10] ; 读取输出数据寄存器 ORR R1, R1, #0x01 ; 点亮LED(PA0置1) STR R1, [R0, #0x10] ; 写入数据 BL Delay ; 调用延时子程序(LR自动保存返回地址) B Loop  ; 循环

1.2 SRAM空间布局

在这里就不得不引申一下SRAM空间的布局。

布局模型​ 栈位置 堆位置 空闲内存位置 适用场景 ​栈在RAM顶端(理论)​​ RAM最高地址 全局变量后 堆与栈之间 多数STM32默认配置 ​栈在堆之后(你的案例)​ 堆结束后 全局变量后 栈之后到RAM结束 部分编译器或手动配置

之前一直理解的这种模型:

全局变量​ → ​堆区(Heap)​​ → ​栈区(Stack)​​ → ​剩余空闲内存

ARM单片机启动流程(三)(栈空间综合理解及相关实际应用)-CSDN博客

这种布局具有一定的风险:

  1. 栈溢出风险更高

    在此布局中,​栈与堆直接相邻,无空闲内存缓冲。一旦发生以下情况,立即导致数据损坏:

    • 函数调用过深(如递归失控)

    • 局部变量过大(如 char buf[512]

    • 堆动态增长(如 malloc 频繁分配)

    解决方案​:

    • 通过 .map 文件监控栈使用量(如 Stack_Usage 段)

    • 增加栈安全余量(如额外预留 20%)

  2. 验证实际 RAM 边界

    检查链接脚本或芯片手册确认 RAM 结束地址(如 STM32F103C8T6 的 RAM 结束于 0x20005000),确保栈未超出范围。

  3. 强制栈到 RAM 顶端的方法

    如需传统布局,在启动文件中显式指定栈顶地址:

ADDR_STACK_TOP EQU 0x20005000 ; 假设RAM结束地址AREA |.ARM.__AT_0x20004800|, DATA, NOINIT, READWRITE, ALIGN=3Stack_Mem SPACE Stack_Size__initial_sp

还有另外一种模型,就是栈在RAM顶端理论。

STM32F103RBT6(RAM 总大小 20KB)

  • RAM 地址范围​:0x20000000 ~ 0x20005000

  • 栈大小​:0x800(2KB)

  • 堆大小​:0x200(512B)

  • 内存布局​(通过链接脚本强制定义):

内存区域​ ​起始地址​ ​结束地址​ ​大小​ ​说明​ ​全局变量区0x20000000 0x200045FF 18KB .data + .bss 段 ​堆区 (Heap)​0x20004600 0x200047FF 512B 动态内存分配区 ​栈区 (Stack)​0x20004800 0x20004FFF 2KB 栈空间(向下生长) ​栈顶 (SP初始值)​0x20005000 - - 栈起始位置(RAM 最高地址)

关键点​:

  • 栈顶 __initial_sp = 0x20005000(RAM 结束地址)。

  • 栈底 = 0x20005000 - 0x800 = 0x20004800

  • 函数调用时,局部变量从 0x20005000 开始向低地址分配(如 0x20004FFC 存储第一个变量)。

假设芯片(RAM 总大小 64KB)

  • RAM 地址范围​:0x20000000 ~ 0x20010000

  • 栈大小​:0x400(1KB)

  • 布局​(默认启动文件配置):

    • 全局变量区​:0x20000000 ~ 0x20000057(88B)

    • 堆区​:0x20000058 ~ 0x2000006B(20B + 对齐填充)

    • 栈区​:0x2000FC00 ~ 0x2000FFFF(1KB)

    • 栈顶​:__initial_sp = 0x20010000(RAM 最高地址)

操作过程​:

  1. 程序启动时,硬件自动加载 0x20010000 到栈指针(SP)。

  2. 函数调用时,SP 从 0x20010000 递减(如分配局部变量后,SP = 0x2000FFFC)。

为什么栈在 RAM 顶端?​

  1. 避免关键区域覆盖
    • 低地址存放中断向量表、内核数据等关键内容。栈在高地址向下生长,溢出时优先覆盖空闲区域而非系统数据。
  2. 与堆隔离
    • 堆向高地址生长(如 malloc0x20000000 开始分配),栈从顶端向下生长,两者相向而行,最大化利用空闲内存。
  3. 硬件支持
    • ARM Cortex-M 的栈指针(SP)复位时从向量表加载,默认指向 RAM 末端。

栈顶 = RAM 结束地址​(如 0x20005000),​栈底 = 栈顶 - 栈大小

1.3 关于内存地址的划分

而针对内存空间的核心机制是通过地址总线和数据总线实现物理寻址,配合多种寻址方式解析指令中的操作数位置

以8051为例,其地址空间分为四类:

空间类型​ ​地址范围​ ​物理映射​ ​访问方式​ ​CODE​ 0x0000~0xFFFF 程序存储器(Flash) 只读,用于指令执行 ​DATA​ 0x00~0x7F 片内RAM低128字节 直接/间接寻址 ​SFR​ ​0x80~0xFF​ ​片内RAM高128字节​ ​直接寻址​ ​XDATA​ 0x0000~0xFFFF 外部扩展RAM 间接寻址(MOVX指令)
  • 关键点​:

    • SFR区域(0x80~0xFF)与DATA空间的高地址物理隔离,但共享相同地址范围​(8051中通过指令类型区分访问)。

    • 若误将变量分配至SFR区域(如char idata var1 _at_ 0x80;),会覆盖硬件寄存器,导致外设失控。

单片机的内存空间分为四类,访问方式各异:

存储类型​ ​访问方式​ ​典型指令​ ​特点​ ​内部RAM​ 直接地址或寄存器间接寻址 MOV A, 30H 高速访问,用于临时变量 ​SFR​ 直接寻址(寄存器名或地址) MOV P1, #0FFH 控制外设,地址固定 ​外部RAM​ 寄存器间接寻址(MOVX指令) MOVX @DPTR, A 需外部扩展,速度较慢 ​程序ROM​ 变址寻址(MOVC指令) MOVC A, @A+DPTR 只读,存储代码和常量

二、单片机寻址方式

单片机通过7种寻址方式确定操作数位置:

  1. 立即数寻址

    • 操作数直接嵌入指令(如MOV A, #30H),用于加载常量。
  2. 直接寻址

    • 指令包含操作数的物理地址(如MOV A, 40H访问内部RAM 40H单元)。
  3. 寄存器寻址

    • 操作数在CPU寄存器中(如ADD A, R0),速度最快。
  4. 寄存器间接寻址

    • 寄存器存储操作数地址(如MOV A, @R0),用@标记,灵活访问RAM。
  5. 变址寻址

    • 基址寄存器(DPTR/PC)+ 变址寄存器(A)生成地址,专用于读取ROM(如MOVC A, @A+DPTR)。
  6. 相对寻址

    • PC当前值 + 偏移量实现跳转(如SJMP 08H),用于分支控制。
  7. 位寻址

    • 直接操作位地址(如SETB 20H.1),适用于标志位控制。

立即数寻址

操作数(称为立即数)直接嵌入指令代码中,而非存储在寄存器或内存地址中。程序执行时,CPU 可直接从指令流中获取操作数,无需额外访问内存或寄存器。

以典型五级流水线为例(取指→译码→执行→访存→写回):

  1. 取指(IFU)​

    CPU根据程序计数器(PC)从内存中读取整条指令​(包含操作码和操作数),送入指令寄存器。

    示例:指令 MOV A, #30H 被完整读取,其中 #30H 是嵌入指令的立即数。

  2. 译码(IDU)​

    CPU“拆包”:解析指令的二进制编码,分离出操作码​(如 MOV)和操作数​(如 30H)。

    关键动作:译码器识别到操作数是立即数,无需从寄存器或内存查找,直接提取其值。

  3. 执行(EXU)​

    “即用”阶段:立即数直接送入算术逻辑单元(ALU)参与运算。

    例如:执行 ADD AX, 10 时,立即数 10 直接与AX寄存器的值相加,无需额外数据访问。

类比快递:理解“写”与“拆包”​

  • ​“写”在指令中​:如同快递包裹里直接放入物品​(操作数),而非存放物品的取件码(地址)。

    MOV A, #30H → 包裹内是物品“30H”;

    MOV A, 30H → 包裹内是取件码“30H”(需根据地址去内存取物品)。

  • ​“拆包即用”​​:

    CPU拆开包裹(译码)后,发现物品已在其中(立即数),直接使用;若包裹里是取件码(内存地址),需额外跑腿(访存)取物。

直接寻址

寄存器寻址

时间很宽,可以做到一个时钟周期,因为都是基于硬件电路实现的。

寄存器间接寻址

流水线中的直接寻址处理流程​

典型的五级流水线包括:​取指(IF)→ 译码(ID)→ 执行(EX)→ 访存(MEM)→ 写回(WB)​。直接寻址的关键操作集中在 ​访存(MEM)阶段​:

  1. 取指(IF)​​:从内存读取指令,程序计数器(PC)更新。

  2. 译码(ID)​​:解析操作码,识别直接寻址模式,提取地址码字段(如 1000H)。

  3. 执行(EX)​​:此阶段通常不进行实际运算(因操作数未就绪),仅传递地址。

  4. 访存(MEM)​​:​核心阶段​!根据地址码访问内存获取操作数(如读取 1000H 处的数据)。

  5. 写回(WB)​​:将操作数写入目标寄存器(如 AL)。

关键流程示例​:
MOV AL, [1000H] 在流水线中的执行:

  • IF:取指令 → ID:解析地址 1000H → EX:无操作 → MEM:读内存 1000H → WB:写 AL

寄存器寻址 和直接寻址 可以理解为 直接寻址是通过内存空间的地址进行相关操作,但是寄存器是CPU的核心寄存器没有在内存地址中对应,只能通过指令操作码和逻辑编号。

而寄存器间接寻址是在倒一手,R0里面存的是地址,然后把这个地址给取出来,然后访问该地址的数据。

寻址方式​ ​操作数来源​ ​示例指令​ ​特点​ ​立即数寻址​ 指令自身携带数据 MOV A, #30H 速度快,但数据不可修改 ​直接寻址​ 内存中的固定地址 MOV A, 45H 需访问内存,地址固定 ​寄存器寻址​ CPU内部寄存器 MOV A, R1 速度快,操作数在寄存器中 ​间接寻址​ 寄存器指向的内存地址 MOV A, @R0 需两次访问(先取地址再取值)

专栏介绍

嵌入式通信协议解析专栏
PID算法专栏
C语言指针专栏
单片机嵌入式软件相关知识
FreeRTOS源码理解专栏



文章源码获取方式:
如果您对本文的源码感兴趣,欢迎在评论区留下您的邮箱地址。我会在空闲时间整理相关代码,并通过邮件发送给您。由于个人时间有限,发送可能会有一定延迟,请您耐心等待。同时,建议您在评论时注明具体的需求或问题,以便我更好地为您提供针对性的帮助。

【版权声明】
本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议。这意味着您可以自由地共享(复制、分发)和改编(修改、转换)本文内容,但必须遵守以下条件:
署名:您必须注明原作者(即本文博主)的姓名,并提供指向原文的链接。
相同方式共享:如果您基于本文创作了新的内容,必须使用相同的 CC 4.0 BY-SA 协议进行发布。

感谢您的理解与支持!如果您有任何疑问或需要进一步协助,请随时在评论区留言。