FreeRTOS堆实现是否违反C别名规则?

6yt4nkrj  于 2023-05-16  发布在  其他
关注(0)|答案(3)|浏览(221)

查看FreeRTOS中堆1的code...

#if ( configAPPLICATION_ALLOCATED_HEAP == 1 )

/* The application writer has already defined the array used for the RTOS
* heap - probably so it can be placed in a special segment or address. */
    extern uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
#else
    static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
#endif /* configAPPLICATION_ALLOCATED_HEAP */

...我们看到堆只是uint8_t对象的数组。
但是,在它的void* pvPortMalloc(size_t xWantedSize)函数中,它定义了一个名为pucAlignedHeapuint8_t*和一个名为xNextFreeBytesize_t
我们的返回值pvReturn然后在这个块中定义…

/* Check there is enough room left for the allocation and. */
        if( ( xWantedSize > 0 ) &&                                /* valid size */
            ( ( xNextFreeByte + xWantedSize ) < configADJUSTED_HEAP_SIZE ) &&
            ( ( xNextFreeByte + xWantedSize ) > xNextFreeByte ) ) /* Check for overflow. */
        {
            /* Return the next free byte then increment the index past this
             * block. */
            pvReturn = pucAlignedHeap + xNextFreeByte;
            xNextFreeByte += xWantedSize;
        }

...然后被程序员用来存储他们想要的任何数据:

//Some example:
my_struct* x = pvPortMalloc(sizeof(my_struct));

但是由于底层数据类型是一个uint8_t数组,这是否意味着堆的任何真实的使用都违反了C的别名要求?
如果这是真的,那么为什么他们被允许违反这些要求而不用担心UB?FreeRTOS几乎不是一个小的爱好项目,所以他们必须知道他们在做什么,但它肯定看起来像这是UB。为什么他们能做到,我却不能?他们似乎没有定义-fno-strict-aliasing,所以我不认为是这样。

cbeh67ev

cbeh67ev1#

因为有许多任务永远不需要回收存储以在其生命周期内保存多个不相关类型的对象的能力,所以C标准并不要求所有实现都支持这种回收。该标准允许实现通过支持超出规定的使用模式来扩展语言,并且任何适合于在其生命周期内需要回收存储的任务的实现都必须以这种方式扩展语言。然而,标准放弃了对此类事情的管辖权。
在clang和gcc处理的语言中,当不使用-fno-strict-aliasing时,一旦通过非字符类型的左值写入任何存储,该存储将其作为“对于该访问和不修改存储值的后续访问”的有效类型。因为这句话没有说“所有后续访问,直到存储的值被修改使用一些其他类型”,没有办法有效地改变存储的有效类型,一旦它已经被写入。可以使用其他类型写入存储,但是在使用两个或更多个不兼容的非字符类型写入存储之后,使用任何非字符类型读取它的任何尝试将与写入存储的有效类型中的至少一个不兼容,并且因此调用UB。
因此,所有在其生命周期内将存储重新用作不同类型的代码都将违反标准中给出的别名约束,除非它将自己限制为使用字符类型读取。然而,程序员不应该为了满足这样的约束而跳来跳去,因为适合于需要重用存储的任务的实现将支持这样的任务,而不管标准是否要求它们这样做。不幸的是,该标准没有提供关于应该支持什么结构的指导,将这种支持视为实现质量问题。

nnsrf1az

nnsrf1az2#

为什么他们能做到,我却不能?
你可以...但这是有代价的。
C标准定义了许多行为规则,任何符合标准的实现都必须遵守这些规则。
此外,C标准为实现留下了许多东西。这称为实现定义的行为。引用自C标准20111的草案N1570:
3.4.1实现定义的行为未指定的行为,其中每个实现记录如何做出选择
最重要的是,该标准具有未定义行为的概念。
3.4.3未定义的行为使用不可移植的或错误的程序结构或错误数据时的行为,对此,本国际标准没有提出要求
对于未定义的行为,注解如下:
可能的未定义行为包括完全忽略情况并产生不可预测的结果,到在翻译或程序执行期间以环境特有的记录方式行为(发出或不发出诊断消息),到终止翻译或执行(发出诊断消息)。
现在,如果你愿意编写只能在特定实现/paltforms/环境中使用的C代码,你可以编写非标准兼容的代码,只要目标实现定义了行为,这些代码就可以正常工作。没问题。有很多代码在做这件事。
代价是你的代码不能在任何实现上使用。
顺便说一句:
问题中提到的具体代码使用uint8_t。通过这样做,代码仅限于在支持uint8_t的实现上使用。正如N1570的“7.20.1.1精确宽度整数类型”中所写的那样,C标准并不要求所有实现都实现该类型。

ars1skjm

ars1skjm3#

当分配例程在没有链接时优化的情况下在单独的转换单元中被编译时,则编译器在编译其他转换单元时不具有关于它们返回指向什么对象类型的指针的信息。
当编译器正在编译使用存储器分配例程的转换单元时,该转换单元可能稍后将与符合C别名规则的转换单元链接。特别地,它可以与返回指向动态分配的内存的指针的对象模块链接,该动态分配的内存最初没有有效类型。因此,编译器必须为当前的翻译单元生成一个目标模块,如果它与这样的模块链接,它将正确工作。
C标准中的别名规则的作用是允许编译器优化一些接收指向不同类型对象的指针的代码。例如,给定一个例程void foo(int *p, float *q),编译器可以假设pq指向不同的内存,因此它可以交换p[i]q[j]上的操作。当存储器分配例程在单独的转换单元中时,关于其返回的地址,这种情况永远不会出现,因此不存在来自别名规则的影响。

相关问题