STM32相同的while循环代码但编译成不同的汇编代码
Posted
技术标签:
【中文标题】STM32相同的while循环代码但编译成不同的汇编代码【英文标题】:STM32 same while loop code but compiled to different assembly code 【发布时间】:2020-06-28 16:03:30 【问题描述】:我正在 stm32F411RE 板 (Cortex-M4) 上学习 RTOS。我使用 MDK uVision v5。我遇到了 C 代码 while 循环 的问题。下面的代码在我的项目和讲师的项目(在 Udemy 上)中完全相同,但是,在编译两个项目(在我的 PC 上)之后,汇编代码看起来不同。我想问是什么让这与众不同。谢谢。
void osSignalWait(int32_t *semaphore)
__disable_irq();
while(*semaphore <=0)
__disable_irq();
__enable_irq();
*semaphore -= 0x01;
__enable_irq();
在调试视图中(见图),如果条件不匹配,它不会去加载真实值LDR r1,[r0, #0x00]然后做比较。相反,它比较并去执行while循环内的命令。 我的代码编译如下
100: void osSignalWait(int32_t *semaphore)
101:
0x08001566 4770 BX lr
102: __disable_irq();
103: while(*semaphore <=0)
104:
0x08001568 B672 CPSID I
101:
102: __disable_irq();
103: while(*semaphore <=0)
104:
0x0800156A 6801 LDR r1,[r0,#0x00]
0x0800156C E001 B 0x08001572
105: __disable_irq();
0x0800156E B672 CPSID I
106: __enable_irq();
107:
108: *semaphore -= 0x01;
0x08001570 B662 CPSIE I
0x08001572 2900 CMP r1,#0x00
0x08001574 DDFB BLE 0x0800156E
0x08001576 1E49 SUBS r1,r1,#1
109: __enable_irq();
0x08001578 6001 STR r1,[r0,#0x00]
0x0800157A B662 CPSIE I
110:
如果我编译讲师(在 Udemy 上)的代码(在我的 PC 上使用他的项目),汇编代码看起来会有所不同(使用完全相同的 while 循环代码)。它将再次加载实际值并进行比较。 教练代码编译如下(在我的电脑上编译)
100: void osSignalWait(int32_t *semaphore)
101:
0x08000CDE 4770 BX lr
102: __disable_irq();
0x08000CE0 B672 CPSID I
103: while(*semaphore <=0)
104:
0x08000CE2 E001 B 0x08000CE8
105: __disable_irq();
0x08000CE4 B672 CPSID I
106: __enable_irq();
107:
0x08000CE6 B662 CPSIE I
0x08000CE8 6801 LDR r1,[r0,#0x00]
0x08000CEA 2900 CMP r1,#0x00
0x08000CEC DDFA BLE 0x08000CE4
108: *semaphore -= 0x01;
0x08000CEE 6801 LDR r1,[r0,#0x00]
0x08000CF0 1E49 SUBS r1,r1,#1
0x08000CF2 6001 STR r1,[r0,#0x00]
109: __enable_irq();
110:
111:
0x08000CF4 B662 CPSIE I
112:
【问题讨论】:
不同的编译器版本?不同的编译器选项? 你好,因为老师从不回复学生,所以我要在这里提问。 @PaulOgilvie 我需要查看哪个编译器选项?谢谢 @Dung-Yi 在讲师的代码图像中,您没有显示函数的第一行。是的,这对我们很重要,因为我们不能假设任何事情。 FWIW:我认为 while 循环的主体应该是__enable_irq(); __disable_irq();
,按 顺序排列。
【参考方案1】:
由于您没有告诉编译器 semaphore
可以在执行此函数期间更改,因此您的编译器已决定优化您的代码并仅加载信号量的值一次并在 while 循环中使用其副本,然后仅最后写出结果。正如现在所写的那样,编译器没有理由认为这可能是有害的。
要通知编译器变量可以在函数外发生变化,在函数执行期间,请使用volatile
关键字,参见:
https://en.cppreference.com/w/c/language/volatile
在这种情况下,您的代码将变为:
void osSignalWait(volatile int32_t *semaphore)
__disable_irq();
while(*semaphore <=0)
__disable_irq(); // Note: I think the order is wrong...
__enable_irq();
*semaphore -= 0x01;
__enable_irq();
顺便说一句,调用__disable_irq
两次(一次在while 循环之前,然后在循环内部开始)然后__enable_irq
似乎有点不稳定,你不是说启用(并做某事)然后禁用while 循环?
【讨论】:
@Dung-Yi:调试器不知道这一点。它只是重新加载,因为它必须是在没有优化的情况下编译的,所以编译器处理 everything 有点像volatile
。见Why does clang produce inefficient asm with -O0 (for this simple floating point sum)?。或者专门针对您和您的讲师代码中的错误:MCU programming - C++ O2 optimization breaks while loop / Multithreading program stuck in optimized mode but runs normally in -O0
顺便说一句,用于内核/线程之间通信的可移植 C 选择是 _Atomic int32_t *
(带有 memory_order_relaxed)。 (如果您要禁用 IRQ,则可以使用单独的原子加载和原子存储,而不是 -=
)。 @Dung-Yi:嗯,我认为您的代码已损坏:while()
循环体在执行-=
之前留下中断启用。我认为您希望 enable(); disable();
作为循环体,让中断处理程序有机会在您旋转等待时运行。或者更好的是,在启用中断的情况下旋转直到看到*sem <= 0
,然后尝试禁用、检查和递减。
@Elijan9 - 循环体当前禁用中断,然后启用它们,正如 Peter Cordes 所提到的,这是倒退的。循环体中的那些(当以正确的顺序完成时)的目的是确保当信号量可用时,循环体退出并禁用中断。同时,重复启用它们可确保在旋转等待期间可以服务中断。
非常感谢你们。基于我的简单而有点愚蠢的问题,我感谢您的好意。 (我做了将近 1 周的大量研究,但找不到答案的正确关键字。我希望问题不是重复的)
@Dung-Yi 我并不是说使用“合作自旋锁”。我不知道您使用的是哪种 RTOS;也许是自制的? (你的教授写的吗?)任何好的商业 RTOS 都会有一个“等待信号量”API 函数,让你的线程进入睡眠状态,直到信号量被另一个线程或中断提供。当您的线程处于休眠状态时,它根本不消耗 CPU 指令周期。使用 yield 或 delay 函数比普通旋转要好,但它仍然需要间歇性的 CPU 周期;它不如专门用于等待信号量的 API 函数。【参考方案2】:
这个众所周知的 keil over 优化错误。多次举报。有内存破坏器,它应该每次都读取内存。
这里是一个例子 clobbers 是如何工作的
#include <stdint.h>
unsigned x;
volatile unsigned y;
int foo()
while(x < 1000);
int bar()
while(x < 1000) asm("":::"memory");
foo:
ldr r3, .L5
ldr r3, [r3]
cmp r3, #1000
bxcs lr
.L3:
b .L3
.L5:
.word x
bar:
ldr r1, .L11
ldr r2, .L11+4
ldr r3, [r1]
cmp r3, r2
bxhi lr
.L9:
ldr r3, [r1]
cmp r3, r2
bls .L9
bx lr
.L11:
.word x
.word 999
【讨论】:
这不是过度优化的错误,它是 C 源代码中的数据竞争 UB,您可以使用内存屏障或易失性解决此问题。或者实际上 avoid UB 使用_Atomic
(使用 mo_relaxed
以便它可以编译为相同的 asm)。为什么说屏障比volatile
或_Atomic int
(带mo_relaxed)更好?由于这是用于仅用于同步的信号量对象,因此您永远不想让编译器将其保存在多个读取的寄存器中。 volatile
确保我们没有来自一次读取的invented loads。
感谢您指出问题。我之前确实在堆栈溢出中发现了这个错误,但从未意识到我的问题是由这个引起的!谢谢。
@PeterCordes 我没有说什么更好。它们不相同,用途不同godbolt.org/z/aXSMB_
@Dung-Yi:说清楚一点;这不是 compiler 错误,而是您的代码中的错误。编译器按预期工作并将变量优化到寄存器中,假设没有其他线程可以修改它们,除非您另有说明。允许假设非_Atomic
和非volatile
变量因为otherwise that would be data-race UB。如果编译器不这样做,任何使用全局变量甚至指针的代码都会运行缓慢,在每次更改后存储并在每次使用前重新加载。
@P__J__:我想我在另一个答案的评论线程和这个之间搞混了。但是,当已经有答案建议 volatile
时,为什么还要发布它呢?有趣的一点是,应该是__disable_irq()
定义的一部分的"memory"
屏障应该已经阻止了重新排序,就像它在GCC 上所做的那样。 godbolt.org/z/fbpe2d。 (但是是的,我知道内存屏障不会影响非转义的本地变量,例如函数 arg。)以上是关于STM32相同的while循环代码但编译成不同的汇编代码的主要内容,如果未能解决你的问题,请参考以下文章