【问题标题】:Instrumenting memory IO in C/C++ for hardware emulation在 C/C++ 中检测内存 IO 以进行硬件仿真
【发布时间】:2015-11-22 19:45:33
【问题描述】:

好的,关于什么和为什么的背景?

我想在桌面 linux 上编译和运行微控制器固件(裸机,无操作系统)。我不想写字节码解释器或二进制翻译器;我想编译原始源代码。将固件作为标准 GUI 应用程序运行具有许多优势,例如快速开发迭代、高级调试、自动化测试、压力测试等。我之前在一些项目中使用 AVR 微控制器完成了此操作,通常采取以下步骤:

  • 提供桌面上不存在的硬件相关标头(主要是 MMIO 寄存器定义 -> 全局变量)
  • 实现外设仿真代码(lcd、eeprom)
  • 做一些反映原始设备用户界面(液晶显示器、按钮)的 GUI
  • 把所有东西粘在一起

前 3 个步骤很简单(AVR 的代码不多),最后一个步骤很棘手。 FW 中的一些构造在桌面版本中最终成为无限循环(例如,忙循环等待外围寄存器更改或中断处理程序更改内存),其他构造最终成为无操作(写入 MMIO,在真实系统上触发某些事情) ,并且将固件的主循环与 GUI 库的主循环融合起来也需要一些创造力。如果 FW 分层很好,那么低级代码可以用胶水函数代替,而无需过多修改。

虽然这些更改会影响整体行为,但我发现最终结果在许多情况下都非常有用。不幸的是,这种方法具有侵入性(修改固件),并且粘合逻辑高度依赖于固件的架构(每次都需要重新发明)。

越来越接近问题...

从 C/C++ 的角度来看,固件与在适当操作系统上运行的代码之间最重要的区别是 MMIO。 MMIO访问有副作用,读写的副作用不同。在桌面应用程序中,这个概念不存在(除非你从用户空间戳硬件)。如果可以在读取或写入内存位置时定义一个钩子,这将启用适当的外围设备仿真,并且固件可以大部分完整地编译。当然,这不能在 C++ 中完成,母语的全部目的就是反对这一点。但是内存调试器在仪器的帮助下使用相同的概念(跟踪内存访问运行时)。

我对实现有一些想法,所以我的问题是您认为它们有多可行,或者有没有其他方法可以达到相同的结果?

  1. 根本没有仪器。如果内存位置被访问,x86 可以发出信号,并且调试器使用它来实现观察点(内存访问中断)。作为概念证明,我创建了这个测试程序:

    #include <stdio.h>
    volatile int UDR;
    
    void read()  { printf("UDR read\n"); }
    void write() { printf("UDR write\n"); }
    
    int main()
    {
        UDR=1;
        printf("%i\n", UDR);
        return 0;
    }
    

    UDR 是我要跟踪的 MMIO 寄存器,如果我使用以下脚本在 GDB 下运行编译程序:

    watch UDR
    commands
    call write()
    cont
    end
    
    rwatch UDR
    commands
    call read()
    cont
    end
    

    结果正是我想要的:

    UDR write
    UDR read
    1
    

    问题是我根本不知道这是否可扩展。据我所知,观察点是有限的硬件资源,但无法找到 x86 的限制。我可能需要不到 100 个。GDB 也支持软件观察点,但仅用于编写,因此它实际上不能用于此目的。代码只能在 GDB 会话下运行的另一个缺点。

  2. 运行时检测。如果我是正确的,Valgrind/libvex 会这样做:读取编译的二进制文件并在内存访问位置(以及许多其他位置)插入检测代码。我可以编写配置了地址和回调的新 Valgrind 工具作为上述 GDB 脚本,并执行应用程序 Valgrind 会话。你认为这可行吗?我找到了一些关于创建新工具的文档,但这似乎并不容易。

  3. 编译时检测。 clang 和 gcc 中的内存和地址清理程序就是这样工作的。这是一个由两部分组成的游戏,编译器发出检测代码,并且一个消毒库(实现实际检查)链接到应用程序。我的想法是用执行上述回调的自己的实现替换 sanitizer 库,而不进行任何编译器修改(这可能超出了我的能力)。不幸的是,我没有找到太多关于检测代码和 sanitizer 库如何交互的文档,我只找到了描述检查器算法的论文。

这就是我的问题,对任何主题的任何评论都表示赞赏。 :)

【问题讨论】:

  • 选择一种语言。 C 不是 C++。特别是,C++ 类允许您重载运算符。 UDR=1可以拨打MMIO_t::operator=(int)
  • 我从未使用过这样的模拟器,而是在实际硬件上使用 JTAG 调试。 “在模拟器上工作但不在设备上工作”的帖子数量令人印象深刻......
  • 对于一个设备我有 C 固件,对于另一个我有 C++11。 MMIO 寄存器是简单的 uint8_t、uint16_t 易失性全局变量(在固件中具有固定地址),使用插桩处理这些变量将与语言无关。为 MMIO 定义自定义类型并为这种类型定义 op=() 是个好主意,我没有想到。但它只允许捕获写入,对于繁忙的等待循环,我也需要捕获读取。无论如何,我会试一试。谢谢!
  • @MartinJames 是的,我有 JTAG,我在正常开发中使用它。用一堆电缆将我的笔记本电脑、JTAG、目标板和工作台电源相互连接起来有点笨拙,但这对于固定工作台来说是可以的。我想在旅途中拥有一个开发选项。正如我所说,桌面调试和测试功能比 JTAG 提供的要好很多。我也知道仿真/模拟不是真实的,这也不是这个实验的目标。
  • 好吧,我不得不承认我的办公室有几根电缆……好吧,太多了,以至于我不会走到长凳下面,因为有东西掉出来了,我可能再也无法让它工作了, (还有一些我不想见的东西住在下面:)。 JTAG 为我提供了源代码级断点和步进、堆栈、寄存器、变量和内存检查 - 我可以接受。

标签: c++ c instrumentation emulation


【解决方案1】:

看看 MSalters 的 cmets 和 Mats 的回答,我显然使这个话题复杂化了。由于我可以访问源代码,因此有一些语言级别的功能可以比使用仪器更容易地挂钩 MMIO 操作。我用一个极简的串行回显示例评估了提议的版本:

#include <avr/io.h>

void mainloop(volatile uint8_t* reg) {
    while(1) {
        loop_until_bit_is_set(UCSRA, RXC);
        uint8_t tmp = *reg;
        *reg = tmp+1;
        loop_until_bit_is_set(UCSRA,  TXC);
    }
}

int main(void) {
    UCSRB = _BV(RXEN) | _BV(TXEN);  // enable UART rx/tx
    UBRRL = 12;                     // 12: 38400 @8Mhz 0.2% error

    mainloop(&UDR);
}

它在串行端口上接收一个字节并发送该字节加一。它具有常见的 MMIO 寄存器用例,包括将寄存器指针传递给函数。

C 方式

在这种情况下,所有 MMIO 访问都用宏包装,这些宏最终对生产代码是无操作的,但在模拟中调用挂钩函数。一个寄存器是mmio8_t,它是一个非整数类型,所以忘记放宏会导致编译时错误。

#if 0   // this is the Production mode
#include <avr/io.h>

//MMIO macros are no op in production
#define MMIO_READ(mmio_reg) mmio_reg
#define MMIO_WRITE(mmio_reg, data) mmio_reg=data

typedef volatile uint8_t mmio8_t;
#endif

#if 1   // this is the Emulation mode
#include <stdio.h>
#include <stdint.h>
// register bit definitions for UCSRA and UCSRB skipped to shorten code sample

struct st_mmio8 {
    const char * name;
    // uint8_t value;
    // emulation hooks for the register
};
typedef const struct st_mmio8 mmio8_t;

mmio8_t UCSRA = { "UCSRA" };    //these are THE mmio registers
mmio8_t UCSRB = { "UCSRB" };
mmio8_t UBRRL = { "UBRRL" };
mmio8_t UDR = { "UDR" };

// some bit magic taken from <avr/io.h>
#define _BV(bit) (1 << (bit))
#define bit_is_set(sfr, bit) (MMIO_READ(sfr) & _BV(bit))
#define loop_until_bit_is_set(sfr, bit) do { } while (bit_is_clear(sfr, bit))

uint8_t MMIO_READ(mmio8_t addr) {
    printf("MMIO_READ id: %s\n", addr.name);
    return _BV(RXC) | _BV(TXC);
}

void MMIO_WRITE(mmio8_t addr, uint8_t val) {
    printf("MMIO_WRITE id: %s\n", addr.name);
}
#endif

void mainloop(mmio8_t* reg) {
    while(1)     {
        loop_until_bit_is_set(UCSRA, RXC);
        uint8_t tmp = MMIO_READ(*reg);
        MMIO_WRITE(*reg, tmp+1);
        loop_until_bit_is_set(UCSRA,  TXC);
    }
}

int main(void) {
    MMIO_WRITE(UCSRB, _BV(RXEN) | _BV(TXEN));  // enable UART rx/tx
    MMIO_WRITE(UBRRL, 12);                     // 12: 38400 @8Mhz 0.2% error

    mainloop(&UDR);
}

MMIO 访问被正确挂钩,但代码的可读性降低,尤其是如果有人习惯了原始样式。

C++ 风格

此选项依赖于 C++ 运算符重载。 mmio_t 类使用类型转换赋值运算符定义为挂钩写入,以及转换运算符挂钩读取:

#if 0   // this is the Production mode
#include <avr/io.h>
using mmio8_t = volatile uint8_t;
#endif

#if 0   // this is the Emulation mode
#include <stdint.h>
#include <iostream>
#include <string>
// register bit definitions for UCSRA and UCSRB skipped to shorten code sample

// some bit magic taken from <avr/io.h>
#define _BV(bit) (1 << (bit))
#define bit_is_set(sfr, bit) (sfr & _BV(bit))
#define loop_until_bit_is_set(sfr, bit) do { } while (bit_is_clear(sfr, bit))

template<typename T>
class mmio_t {
    public:
    mmio_t(const std::string& regname) : regname(regname) {}

    //this is a non-chainable assignment
    void operator=(T data) {
        std::cout << "mmio_write " << regname << std::endl;
    }

    operator T() {
        std::cout << "mmio_read " << regname << std::endl;
        return _BV(TXC) | _BV(RXC);
    }
    private:
    std::string regname;
    //T value;
    //std::function hooks for emulation code
};
using mmio8_t = mmio_t<uint8_t>;

mmio8_t UCSRA("UCSRA");
mmio8_t UCSRB("UCSRB");
mmio8_t UBRRL("UBRRL");
mmio8_t UDR("UDR");

#endif

void mainloop(mmio8_t* reg) {
    while(1) {
        loop_until_bit_is_set(UCSRA, RXC);
        uint8_t tmp = *reg;
        *reg = tmp+2;
        loop_until_bit_is_set(UCSRA, TXC);
    }
}

int main(void) {
    UCSRB = _BV(RXEN) | _BV(TXEN);  // enable UART rx/tx
    UBRRL = 12;                     // 12: 38400 @8Mhz 0.2% error

    mainloop(&UDR);
}

除了引入 mmio8_t 类型外,代码与原版相同,操作也正确捕获。

虽然这些示例不完整或可能不是 100% 正确,但它们显示了每个版本的基本特征。感谢所有提示和想法!

【讨论】:

    【解决方案2】:

    我没有时间回复您问题中的所有问题,但这可能太长了,无法发表评论......

    所以关于调试器中的“观察点”,它们使用调试寄存器,而您可以自己编写代码来使用这些寄存器(有 API 函数可以做到这一点 - 您需要处于内核模式才能写入这些寄存器),正如您所说,您将用完寄存器。这个数字也远低于你的 100。在 x86 处理器中,有 4 个调试位置寄存器,涵盖对 1-8 字节宽位置的读取和/或写入。因此,如果您的 IO 空间总共少于 32 个字节(分布在不超过 4 个块中,每个块不超过 8 个字节),它会起作用。

    选项 2 的问题是您需要保证 IO 寄存器使用的区域不用于应用程序中的其他内容。如果所有 IO 寄存器都在前 64KB 中,这可能很“容易”。否则,您必须尝试确定它是 MMIO 访问还是常规访问。除了编写自己的 Valgrind 版本之外,您也不会立即完成...即使您雇用了最初编写 valgrind 的人...

    选项 3 在匹配地址方面与选项 2 有相同的问题。我的感觉是,这对你没有太大帮助,你最好以不同的方式接近它。

    我在使用过的各种芯片模拟器中看到的方法是将对真实硬件的访问修改为函数调用。您可以通过类似于 MSalters 描述的方法在 C++ 中做到这一点。

    或者通过修改您的代码,例如:

    MMIO_WRITE(UDR, 1);
    

    然后让MMIO_WRITE翻译成:

     #if REAL_HW
     MMIO_WRITE(x, y)   x = y
     #else
     MMIO_WRITE(x, y)  do_mmio_write(x, y)
     #endif
    

    do_mmio_write 能够理解地址以及它们以某种方式做什么。

    这当然是我在工作中使用的 GPU 模型,用于对我们即将制作成硅片的最新、最强大的 GPU 进行建模,并且是我工作的前一家公司使用的模型,它有这样的模型。

    是的,您将不得不重写您的一些代码 - 理想情况下,您的代码是这样编写的,以便您有特定的小部分代码接触实际硬件[如果您想从一种类型迁移,这当然是一个好习惯微控制器到另一个,否则在这种情况下你也必须做更多的重写]。

    正如 Martin James 所指出的,任何此类模拟的问题在于,如果您的实际模拟器不是很好,您会遇到“兼容性问题”——尤其是硬件与软件竞争条件等问题,而您的软件在这些情况下是完美的与模拟硬件模型同步,但真实硬件将与软件异步执行操作,因此您对两个寄存器的两次读取现在将获得与软件模型不同的值,因为您的软件模型在真实硬件中发生了一些任意变化不考虑 - 现在你遇到了一个只在蓝月亮中出现一次的令人讨厌的错误,而且只出现在“无法调试”的硬件变体上,从来没有出现在软件模型中。

    【讨论】:

    • 为了定位 MMIO 寄存器,我想像 GDB 那样使用符号表,而不是确定 MMIO 范围(选项 2 和 3 工具都应该能够访问它)。您基于宏的建议对我来说看起来很有希望。最初我的目标是不修改 FW 源代码,但在 FW 中包含扩展为 noop 的宏是可以接受的。比起 MSalters 建议的涉及 C++ 类型系统,我更喜欢它。关于调试器限制的信息 +1。
    • 至于 sim真正的不兼容性,我知道这些。我故意编写外围仿真(而不是仿真),根本没有设备模型或准确的时序。例如,我对所有这些的目标是在其原始上下文中(或多或少)测试驱动具有大量数据集的 AT 命令集解析器,作为集成过程的一部分。重点是测试自动化和更高级别的功能。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-08-20
    • 1970-01-01
    • 2012-07-19
    • 2016-04-03
    • 1970-01-01
    • 2019-09-09
    相关资源
    最近更新 更多