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 &lt;= 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循环代码但编译成不同的汇编代码的主要内容,如果未能解决你的问题,请参考以下文章

STM32 硬件I2C 死循环 自动退出

转载:将STM32的标准库编译成lib使用图文

如何使用keil5将stm32的hal库编译成lib文件——F1版本

stm32中的步进电机驱动和lcd显示问题

STM32F407串口传输

RTX移植STM32F103,超详细~