【问题标题】:Is it safe to attempt (and fail) to write to a const on an STM32?尝试(但失败)在 STM32 上写入 const 是否安全?
【发布时间】:2019-04-12 09:45:41
【问题描述】:

因此,我们正在尝试一种方法来执行一些矩阵数学运算。这是嵌入式的,因此内存是有限的,而且我们将拥有大型矩阵,因此它有助于我们将其中一些存储在闪存中而不是 RAM 中。

我编写了一个矩阵结构、两个数组(一个 const/flash 和另一个 RAM)以及一个“修改”和“获取”函数。一个矩阵,我初始化为 RAM 数据,另一个矩阵我初始化为闪存数据,使用从 const *f32 到 *f32 的转换。

我发现当我在我的 STM32 嵌入式处理器上运行此代码时,RAM 矩阵是可修改的,指向闪存数据的矩阵根本不会改变(设置为 12.0 不会“占用”,值保持为 2.0)。

(更改前)a=2,b=2,(更改后)c=2,d=12

这是可接受的行为,根据设计,我们不会尝试修改闪存数据矩阵,但如果我们犯了错误,我们不希望它崩溃。

但是,如果我在我的 Windows 机器上使用 Visual C++ 运行相同的代码,当我尝试运行下面的代码时,当我尝试将 const 数组修改为 12.0 时,我会遇到“访问冲突”。

Windows 会反对这并不奇怪,但我想更好地理解行为上的差异。这似乎与 CPU 架构有关。在我们的 STM32 上,让代码尝试写入 const 数组并让它不起作用是否安全?或者是否有副作用,或避免这种情况的原因?

static const f32 constarray[9] = {1,2,3,1,2,3,1,2,3};
static f32 ramarray[9] = {1,2,3,1,2,3,1,2,3};

typedef struct {
     u16 rows;
     u16 cols;
     f32 * mat;
} matrix_versatile;

void modify_versatile_matrix(matrix_versatile * m, uint16_t r, uint16_t c, double new_value)
{
    m->mat[r * m->cols + c] = new_value;    
}

double get_versatile_matrix_value(matrix_versatile * m, uint16_t r, uint16_t c)
{
    return m->mat[r * m->cols + c];
}

double a;
double b;
double c;
double d;

int main(void)
{
    matrix_versatile matrix_with_const_data;
    matrix_versatile matrix_with_ram_data;

    matrix_with_const_data.cols = 3;
    matrix_with_const_data.rows = 3;
    matrix_with_const_data.mat = (f32 *) constarray;

    matrix_with_ram_data.cols = 3;
    matrix_with_ram_data.rows = 3;
    matrix_with_ram_data.mat = ramarray;

    a = get_versatile_matrix_value(&matrix_with_const_data, 1, 1);
    b = get_versatile_matrix_value(&matrix_with_ram_data, 1, 1);
    modify_versatile_matrix(&matrix_with_const_data, 1, 1, 12.0);
    modify_versatile_matrix(&matrix_with_ram_data, 1, 1, 12.0);
    c = get_versatile_matrix_value(&matrix_with_const_data, 1, 1);
    d = get_versatile_matrix_value(&matrix_with_ram_data, 1, 1);    

【问题讨论】:

    标签: visual-c++ embedded stm32


    【解决方案1】:

    但如果我们犯了错误,我们不希望它崩溃。

    尝试写入 ROM 本身不会导致崩溃,但尝试写入它的代码根据定义存在错误,并且在任何情况下都可能崩溃,并且肯定不会按预期运行。

    这几乎是完全错误的想法;如果您有错误,您真的希望它在开发过程中崩溃,而不是在部署之后。如果它默默地做了错误的事情,你可能永远不会注意到这个 bug,或者崩溃可能发生在 bug 附近以外的地方,所以很难找到。

    如果您尝试写入标记为只读的内存,MMU 或 MPU 的架构可能会发出异常。这就是 Windows 中正在发生的事情。在这种情况下,如果异常处理程序通过某种方式报告此类错误,它可能是一个有用的调试帮助。在这种情况下,错误会在发生时准确地报告,而不是稍后在访问一些无效数据或对不正确的结果采取行动时崩溃。

    一些,但不是所有的 STM32 部件都包括 MPU (application note)

    【讨论】:

    • 知道了。是的,在开发时,我们将检查并仔细检查我们从未尝试修改基于 FLASH 的矩阵。但我很好奇,你说我们的 STM32“无论如何都可能崩溃”……你能不能扩展一下,可能会发生什么?
    • 简而言之,我想我认为这是一个功能,CPU 足够聪明,可以意识到它正在修改 FLASH 并且它不会运行。但这是猜测。真正发生了什么,它会如何失败?
    • 哦,关于开发理念,我认为我们将在矩阵结构中添加一个布尔值来指示 RW 或 RO,以提供进一步的保护,这样即使在发生混淆时我们也不会尝试写入 FLASH 并依靠这种“noop”行为。所以在这一点上,我主要是好奇:为什么它没有效果,这是一个特性,我们可以指望它吗,或者这种行为是未定义的、不确定的,并且可能会咬我们?如果它咬我们,怎么办?
    • @Chris 该行为是确定的,已记录,因此您可以将其视为一项功能。将已知值写入已知地址的效果是完全可以预测的。 RAM:值变化,FLASH:无变化且状态位已设置,内存映射寄存器:参见参考手册,未映射区域:总线故障。当地址或值不可预测时,就会出现问题。
    • @chris 防御性代码是一回事,但您的方法毫无意义 - 您正在编写 _more_software 来检查软件错误。是什么让您认为,如果您希望编写损坏的软件,编写更多的软件会检测到它!?对结果或再条件和单元测试进行断言的系统是一种更健壮的方法。您提议编写代码来检测一种特定类型的代码错误;那么所有更可能的错误呢?此外,您举例说明的错误归结为糟糕的编码实践 - 抛弃了一个 const.如果你不这样做,编译器会捕获它。
    【解决方案2】:

    答案可能取决于系列(STM32F1、STM32F4、STM32L1 等),因为它们的闪存控制器有些不同。

    我曾经在 STM32F429 上犯过同样的错误,并调查了一下,所以我可以知道 STM32F4 上会发生什么。

    可能没什么。

    默认情况下,闪存受到保护,以便对这些类型的编程错误具有一定的弹性。为了修改闪存,必须将某些值写入FLASH->KEYR 寄存器。如果写入了错误的值,则闪存将被锁定直到复位,因此除非程序写入 64 位正确值,否则不会发生真正的坏事。不会发生意外中断,因为中断使能位也受此密钥保护。该尝试将在FLASH->SR 中设置一些错误位,因此程序可以对其进行检查并警告用户(最好是测试人员)。

    但是,如果那里有一些代码(例如引导加载程序,或将某些内容登录到闪存中)应该在闪存中写入某些内容,即它使用正确的密钥解锁闪存,然后 坏事可能会发生。

    如果闪存在之前的写操作后保持解锁状态,那么写入之前编程的区域会将位从1 更改为0,但不会从0 更改为1。这意味着闪存将包含旧值和新写入值的按位AND

    如果失败的写入尝试首先发生,然后解锁,则除非首先正确清除状态位,否则任何合法的写入或擦除操作都不会成功。

    如果预期和非预期访问交错发生,例如在中断处理程序中,那么所有的赌注都没有了。


    即使值在不可变闪存中,仍然可能出现意外结果。考虑这段代码

    int foo(int *array) {
      array[0] = 1;
      array[1] = 3;
      array[2] = 5;
      return array[0];
    }
    

    优化编译器可能会识别出返回值应始终为1,并发出相应的代码。或者它可能不会,并从它存储的任何地方重新加载array[0],可能与闪存不同的值。它在调试和发布版本中的行为可能不同,或者在从不同位置调用函数时,因为它的内联方式可能不同。


    如果指针指向一个未映射的区域,既不是 RAM 也不是 FLASH 也不是一些内存映射寄存器,那么会发生一个错误,并且由于默认的错误处理程序只包含一个无限循环,程序将挂起,除非它安装了可以处理这种情况的故障处理程序。不用说,覆盖随机 RAM 区域或寄存器可能会导致难以预测的行为。


    更新

    我已经在实际硬件上尝试过您的代码。当我逐字运行它时,编译器 (gcc-arm-none-eabi-7-2018-q2-update -O3 -lto) 优化了所有内容,因为之后没有使用变量。将a, b, c, d 标记为volatile 导致c=2d=12,它仍在考虑第一个数组const,并且没有生成对数组的访问。 constarray 根本没有出现在地图文件中,链接器已经完全消除了它。

    所以我一次尝试了一些方法来强制优化器生成可以实际访问数组的代码。

    • 禁用优化 (-O0)
    • 制作所有变量volatile
    • 插入几个编译时内存屏障 (asm volatile("":::"memory");
    • 中间做一些复杂的计算

    其中任何一个都对不同的 MCU 产生了不同的影响,但它们在单个平台上始终是一致的。

    • STM32F103:硬故障。只允许对闪存进行半字(16 位)写访问,8 或 32 位总是会导致故障。当我将数据类型更改为short 时,代码运行了,当然对闪存没有任何影响。
    • STM32F417:代码运行,对闪存内容没有影响,但第 6 位和第 7 位、PGPERRPGSERR 中的FLASH->SR 在第一次写入尝试后设置了几个周期constarray.
    • STM32L151:代码运行,对闪存控制器状态没有影响。

    【讨论】:

    • 问题似乎是关于对只读地址的写访问,而不是更具体地说是关于无法写入闪存的问题,因为它被明确写保护。写保护仅在编程 flash 时相关;当不处于编程模式时,访问根本没有影响,除非该部分具有 MPU 并且该区域被标记为只读。标题和代码示例指的是const,而不是专门针对闪存编程的闪存。
    • @Clifford const 数组由链接器放置在闪存中,除非使用一些非常奇怪的链接器脚本或启动代码。如果在将数组地址传递给需要非常量指针参数的函数并尝试写入数组元素时,const 限定符丢失,那么它将尝试将值写入闪存中的地址。当然,闪存内容将保持不变,但闪存控制器将其视为失败的编程尝试,并在其状态寄存器中设置错误标志。
    • @Clifford 它曾经让我想知道为什么我故意编写闪存的代码在调用某个不相关的函数时停止工作。原来不相关的函数与问题中的代码示例具有相同的错误。当我清除FLASH->SR 后,Flash 编程开始工作,监控状态寄存器帮助我找到了真正的错误。
    • 我不同意,在问题中没有尝试在运行时对闪存进行编程。如果您正在在运行时对闪存进行编程,这是一个重要的考虑因素(这种做法在任何情况下都在 STM32 中存在特殊问题),但它显然与这个问题无关。
    • @Clifford 我已经在实际硬件上尝试过,请参阅我更新的答案。闪存控制器并不真正关心程序员尝试做什么,它在写访问时设置标志。或引发故障。
    猜你喜欢
    • 2021-03-10
    • 2021-05-01
    • 2014-09-21
    • 1970-01-01
    • 2017-11-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多