FreeRTOS基础--堆栈概念与汇编指令实战解析

🎬 渡水无言个人主页渡水无言

专栏传送门 : 《linux专栏》《嵌入式linux驱动开发》《freertos专栏

⭐️流水不争先,争的是滔滔不绝

📚博主简介:第二十届中国研究生电子设计竞赛全国二等奖 |国家奖学金 | 省级三好学生

| 省级优秀毕业生获得者 | csdn新星杯TOP18 | 半导纵横专栏博主 | 211在读研究生

在这里主要分享自己学习的linux嵌入式领域知识;有分享错误或者不足的地方欢迎大佬指导,也欢迎各位大佬互相三连

目录

前言

一、常用汇编指令速览

[1.1 读内存:Load](#1.1 读内存:Load)

用于从内存中读取数据到寄存器。

[1.2 写内存:Store](#1.2 写内存:Store)

[1.3 加减运算](#1.3 加减运算)

[1.4 比较指令](#1.4 比较指令)

二、堆与栈的核心概念

2.1、堆,栈的概念

2.1、堆和栈的地址生长方向

[三、出栈 / 压栈汇编指令详解(以 STM32 的 ARM 汇编为例)](#三、出栈 / 压栈汇编指令详解(以 STM32 的 ARM 汇编为例))

[3.1 出栈(恢复现场):从低地址→高地址](#3.1 出栈(恢复现场):从低地址→高地址)

3.2、压栈(保存现场):从高地址→低地址

总结


前言

在嵌入式底层开发(尤其是 RTOS 移植、中断处理)中,堆和栈的内存管理、汇编级的压栈 / 出栈操作是核心基础。本文从堆栈的地址生长方向入手,详解压栈 / 出栈汇编指令的执行逻辑。


一、常用汇编指令速览

ARM 汇编指令是操作硬件的 "最小单元",在 FreeRTOS 任务切换、中断现场保护中随处可见。以下是嵌入式开发中最常用的几类指令:

1.1 读内存:Load

用于从内存中读取数据到寄存器。

复制代码
LDR R0, [R1, #4]  ; 读取地址 R1+4 处的4字节数据,存入 R0

LDR:Load Register,加载寄存器。

R1, #4:基址 + 偏移寻址,计算有效地址为 R1 + 4。

1.2 写内存:Store

用于将寄存器中的数据写入内存。

复制代码
STR R0, [R1, #4]  ; 把 R0 的4字节数据写入地址 R1+4

STR:Store Register,存储寄存器。

与 LDR 相反,实现寄存器到内存的数据回写。

1.3 加减运算

用于寄存器间的算术操作,是任务调度、地址计算的基础。

复制代码
ADD R0, R1, R2   ; R0 = R1 + R2
ADD R0, R0, #1   ; R0 = R0 + 1
SUB R0, R1, R2   ; R0 = R1 - R2
SUB R0, R0, #1   ; R0 = R0 - 1

1.4 比较指令

用于比较两个寄存器的值,结果保存在 PSR(程序状态寄存器)中,为后续条件跳转提供依据。

复制代码
CMP R0, R1       ; 比较 R0 和 R1,结果更新到PSR

1.5 跳转指令

用于程序流控制,是函数调用、任务切换的核心。

复制代码
B    main        ; Branch,直接跳转到 main 标签处
BL   main        ; Branch and Link,先将返回地址存入LR寄存器,再跳转到 main

二、堆与栈的核心概念

堆(Heap)和栈(Stack)是处理器中两种核心的内存分配方式,二者的地址生长方向、操作逻辑差异显著,直接影响程序的内存布局和执行效率。

2.1、堆,栈的概念

注意:我们经常 "堆栈" 混合着说,其实它们不是同一个东西:

堆(heap)就是一块空闲的内存,需要提供管理函数。

malloc:从堆里划出一块空间给程序使用。

free:用完后,再把它标记为 "空闲" 的,可以再次使用。

栈(stack)函数调用时局部变量保存在栈中,当前程序的环境也是保存在栈中。

可以从堆中分配一块空间用作栈。

2.1、堆和栈的地址生长方向

堆(Heap):生长方向向上,即内存地址从低到高扩展。

堆是开发者通过malloc()/free()手动管理的内存区域,分配时从低地址开始,每次申请新内存都会向高地址方向延伸。

栈(Stack):生长方向向下,即内存地址从高到低收缩。

栈是系统自动管理的内存区域(用于存储函数参数、局部变量、寄存器现场),每次压栈操作都会占用更低的内存地址。

不同处理器的堆栈生长方向差异

处理器 / 架构 堆栈生长方向 核心逻辑(以 PUSH 指令为例)
51 单片机 正向生长(向上) 执行PUSH时,先将栈指针SP+1,再将数据入栈
80x86 微机 逆向生长(向下) 执行PUSH时,先将栈指针SP-2,再将 16 位数据入栈(小端模式:高字节存高地址,低字节存低地址)
STM32(ARM) 逆向生长(向下) 执行压栈指令时,栈指针SP先递减,再存储数据;出栈时先读取数据,再递增SP

补充知识点:C 语言函数参数传递遵循 "从右向左入栈" 规则,因此函数最左侧的参数会最后入栈、最先出栈。

三、出栈 / 压栈汇编指令详解(以 STM32 的 ARM 汇编为例)

STM32 基于 ARM 架构,常用ldmia(出栈 / 加载)、stmdb(压栈 / 存储)指令完成寄存器现场的保存与恢复,以下结合流程图和具体地址示例拆解执行逻辑。

3.1 出栈(恢复现场):从低地址→高地址

出栈指令用于将栈中保存的寄存器值恢复到 CPU 寄存器中,地址方向为从下往上(低地址→高地址)

核心指令:ldmia r0!, {r4-r6}

执行示例(假设初始r0=0x04):

将r0指向的地址0x04中的内容加载到r4,随后r0 = r0 + 4 = 0x08;

将r0指向的地址0x08中的内容加载到r5,随后r0 = r0 + 4 = 0x0C;

将r0指向的地址0x0C中的内容加载到r6,随后r0 = r0 + 4 = 0x10;

最终效果:栈中低地址的数据依次恢复到r4→r5→r6,栈指针r0向高地址移动。

3.2、压栈(保存现场):从高地址→低地址

压栈指令用于将 CPU 寄存器的值保存到栈中,地址方向为从上往下(高地址→低地址)

核心指令:stmdb r0!, {r4-r6}

执行示例(假设初始r0=0x10):

先将r0 = r0 - 4 = 0x0C,再将r6的内容存储到r0指向的地址0x0C;

再将r0 = r0 - 4 = 0x08,再将r5的内容存储到r0指向的地址0x08;

最后将r0 = r0 - 4 = 0x04,再将r4的内容存储到r0指向的地址0x04;

最终效果:r4→r5→r6的内容依次保存到栈的低地址区域,栈指针r0向低地址移动。


总结

本期博客从堆栈的地址生长方向入手,详解了压栈 / 出栈汇编指令的执行逻辑。

相关推荐
长江后浪博客15 小时前
ESP32-SOLO-1 在 ESP-IDF v5.5 下启动不断重启问题分析与解决
单片机·嵌入式硬件·嵌入式·esp32·freertos·esp-idf
Jason_zhao_MR1 天前
Linux与采集MCU通信为什么我们用了双SPI
linux·arm开发·单片机·嵌入式硬件·嵌入式
嵌入式阿蔡1 天前
云平台设备接入实战:三元组鉴权、属性上报与命令下发(阿里云/华为云)
嵌入式硬件·物联网·嵌入式·存储
2601_962296511 天前
C/C++:编译全过程——预处理、编译、汇编、链接(包含预处理指令:宏定义,文件包括、条件编译)
汇编·预处理·链接·c/c++·编译过程
分支预测失败2 天前
RISC-V 上下文切换实战:从 Trap 入口到 Linux 进程调度
后端·嵌入式
Freak嵌入式2 天前
Pico PIO 汇编语法深度详解:标识符、指令、伪指令全解析
汇编·stm32·单片机·嵌入式硬件·fpga开发·硬件架构
Freak嵌入式2 天前
基于 MicroPython+DMA 链式触发的 Scatter-Gather 数据聚合实现
嵌入式
Freak嵌入式2 天前
MicroPython ADC 采样拖慢主循环?试试 DMA + 乒乓缓冲,全程解放 CPU
嵌入式
WWJA王文举2 天前
FreeRTOS动态创建任务和静态创建任务详解:xTaskCreate与xTaskCreateStatic区别
操作系统·freertos·嵌入式开发
Freak嵌入式2 天前
从代码到项目:MicroPython+MCP4725 自定义类实现信号发生器功能的完整流程
嵌入式