STM32相同while循环代码编译后会得到不同的汇编代码

3

我正在学习在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上)的项目编译他的代码,汇编代码看起来会有所不同(但是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: } 

3
不同的编译器版本?不同的编译器选项? - Paul Ogilvie
1
嗨,因为讲师从未回复学生,所以我不得不在这里提出问题。 - Dung-Yi
@PaulOgilvie 我需要查看哪个编译器选项?谢谢。 - Dung-Yi
1
@Dung-Yi,在讲师的代码图像中,您没有显示函数的第一行。是的,对我们来说这很重要,因为我们不能假设任何东西。 - Jabberwocky
2
就我个人而言:我认为 while 循环体应该按照 __enable_irq(); __disable_irq(); 的顺序来执行。 - Chris Hall
显示剩余4条评论
2个回答

4
由于在该函数执行期间未告知编译器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();
}

顺便提一下,在 while 循环之前调用 __disable_irq,然后在循环内部开始处再次调用该函数,然后调用 __enable_irq,似乎有些奇怪,难道你的意思不是在 while 循环内启用(并执行某些操作),然后再禁用吗?


2
@Dung-Yi:调试器不知道这个。这只是因为编译时没有启用优化,所以编译器将所有内容都视为 volatile。请参阅 为什么clang使用-O0(针对此简单浮点数总和)会产生低效的汇编代码?。或者特别针对您和您的指导老师代码中的错误:MCU编程-C++ O2优化破解while循环/ 多线程程序在优化模式下卡住但在-O0模式下正常运行 - Peter Cordes
1
@Elijan9 - 循环体当前禁用中断,然后启用它们,正如Peter Cordes所提到的那样,这是相反的。循环体中这些操作(按正确顺序执行)的目的是确保当信号量变为可用时,循环体退出时中断被禁用。同时,重复启用它们可以确保在自旋等待期间可以服务中断。 - phonetagger
1
@Dung-Yi - 自旋等待技术是处理信号量的一种非常原始的方式。在一个良好的抢占式RTOS中,您应该能够在不旋转的情况下等待信号量。自旋浪费CPU资源(指令周期)。操作系统(或RTOS)应该通过调用某些OS API函数来等待/挂起信号量,并在返回时,要么您拥有信号量,要么等待超时(如果您设置了超时)。 - phonetagger
2
@Dung-Yi 我并不是指使用“协作自旋锁”。我不知道你在使用什么样的RTOS;自己编写的吗?(是你的教授写的吗?)任何一个好的商业RTOS都会有一个“等待信号量”的API函数,它会将你的线程置于睡眠状态,直到另一个线程或中断释放该信号量。当你的线程处于睡眠状态时,它根本不会消耗CPU指令周期。使用yield或delay函数比纯自旋要好,但它仍然需要间歇性的CPU周期;它不如专门用于等待信号量的API函数好。 - phonetagger
1
@PeterCordes:为了在自旋时允许中断处理程序运行,通常情况下你需要在循环中使用先启用再禁用的方式,而不是相反。原帖作者使用了相反的顺序,我选择不改变那个顺序,但我已经在我的帖子中提到这似乎有点奇怪,我认为应该是相反的顺序。在检查循环条件时禁用中断,然后在循环内短暂地启用它更有意义... - Elijan9
显示剩余18条评论

0

这是一个非常著名的Keil过度优化错误,已经被多次报告。由于内存破坏,它应该每次读取内存。

以下是一个演示如何使用clobber的示例

#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

1
这不是过度优化的bug,而是C源代码中的数据竞争UB问题,你可以通过存储屏障或者volatile进行解决。或者使用_Atomic(使用mo_relaxed以便编译到相同的汇编代码)来避免UB的出现。为什么你认为栅栏比volatile或者_Atomic int (使用mo_relaxed)更好呢?因为这个信号量对象只用于同步,所以你永远不想让编译器在多次读取之间将其保存在寄存器中。使用volatile可以确保我们没有从一次读取中“发明加载”。 - Peter Cordes
感谢您指出这个问题。我之前在Stack Overflow上找到了这个错误报告,但从未意识到我的问题是由此引起的!非常感谢。 - Dung-Yi
@PeterCordes 我并不说哪个更好。它们并不相同,有不同的用途。https://godbolt.org/z/aXSMB_ - 0___________
@Dung-Yi:只是为了明确一下;这不是编译器的错误,而是你代码中的错误。编译器按照预期工作,并将变量优化到寄存器中,假设没有其他线程可以修改它们,除非你告诉它否则。对于非_Atomic和非volatile变量,它可以假定如此,因为否则就会出现数据竞争UB。如果编译器不这样做,任何使用全局变量甚至指针的代码都会运行缓慢,每次更改后都要存储并在每次使用前重新加载。 - Peter Cordes
@PeterCordes 只是为了展示Keil特定的问题,因为这是一个非常具体的问题。 - 0___________
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接