仅写指针类型

54

我正在为一个嵌入式系统编写软件。

我们使用指针来访问FPGA设备的寄存器。
其中一些寄存器是只读的,而另一些是只写的。

当读取只写寄存器时会产生未定义的值。

我希望定义一个指针类型,使得编译器能够检测到从只写寄存器(也称解除引用)中读取值的操作。

是否可以仅使用C语言语法创建只写指针?
(我们使用C开发第一代原型,但在第二代将转向C++。)

如何在C++中创建一个有效的只写指针? (请记住,这不是跟踪动态内存中的项,而是访问硬件地址。)

在这个嵌入式系统上,安全和质量是最高关注点。


9
我严重怀疑你能否用C语言做到这一点。然而,用C ++,你应该能够相对容易地实现它。 - Sergey Kalinichenko
3
不是这样的。请看杰瑞的回答。 - Konrad Rudolph
6
在C语言中,您可以使用定义良好的读/写函数将它们隐藏在不透明指针后面。 - Konrad Rudolph
2
@Alexey,我不明白你的观点。这就像使用int而不是std::string一样需要纪律:实际上没有什么区别。一旦你声明了适当类型的变量,编译器会确保你不会错误地使用它们。 - Konrad Rudolph
2
我认为我理解并同意Alexey和Konrad的观点。Alexey是正确的,你不能防止它被读取的可能性 - 类似于“const”正确性,你所做的任何事情更像是一个软保证,只需要1个类型转换或错误的指针引用就会被破坏。另一方面,Konrad和其他人是正确的,你可以构建抽象来减少这种可能性 - 在C++中使用智能指针,在C中使用访问器函数和不完整类型等。 - asveikau
显示剩余11条评论
6个回答

61

我可能会为每个类编写一个小包装器类:

template <class T>
class read_only {
    T volatile *addr;
public:
    read_only(int address) : addr((T *)address) {}
    operator T() volatile const { return *addr; }
};

template <class T>
class write_only { 
    T volatile *addr;
public:
    write_only(int address) : addr ((T *)address) {}

    // chaining not allowed since it's write only.
    void operator=(T const &t) volatile { *addr = t; } 
};

假设您的系统有一个合理的编译器,我认为这两者都将被优化,使生成的代码与使用原始指针无异。用法:

read_only<unsigned char> x(0x1234);
write_only<unsigned char> y(0x1235);

y = x + 1;         // No problem

x = y;             // won't compile

不错的模板!您如何将它们组合用于读写寄存器? - Giacomo Tesio
你为什么选择使用 int 而不是 volatile T*?如果你真的想要一个整数,那么可以使用 intptr_t。此外,operator T() 可以是 const volatile - Jon Purdy
3
@JonPurdy:我避免使用T volatile*,因为这意味着用户将拥有一个读/写指向寄存器的指针——这正是我们想要避免的。嵌入式编译器通常受到一定限制,因此期望它们包含intptr_t(在C++11中刚刚添加)是要求较高的。如果必须的话,我会将其作为模板参数。我同意使用const volatile,已经进行了编辑,谢谢。 - Jerry Coffin
如果你想读/写,我不确定你比只使用volatile T *能得到什么好处。我想,如果你愿意,可以创建一些东西来避免显式引用,但与这些模板的关系纯粹是...精神上的--它们都有operator Toperator =来进行读写。 - Jerry Coffin
2
+1. 由于只写很难调试,将写操作隔离到一个函数中可以为您提供一个很好的记录点。我仍然担心轻率地使用int来表示地址。我曾经工作过的最后一个嵌入式系统具有16位整数但32位地址。我会为地址创建一个typedef并使用它,这样可以轻松地将代码重新用于其他系统。 - Adrian McCarthy
2
@AdrianMcCarthy:typedef 也可以使用,但正如我上面所说的,如果我要将其从 int 更改,我可能会使用模板参数。为了测试,您还可以创建一个类来存储(并允许检索)最后写入的值,以便代码的其余部分可以将其视为读/写内存。 - Jerry Coffin

8
我会使用结构体的组合来表示寄存器,并编写一对函数来处理它们。
在一个名为 fpga_register.h 的文件中,您可以添加以下内容:
#define FPGA_READ = 1; 
#define FPGA_WRITE = 2;
typedef struct register_t {
    char permissions;
} FPGARegister;

FPGARegister* fpga_init(void* address, char permissions);

int fpga_write(FPGARegister* register, void* value);

int fpga_read(FPGARegister* register, void* value);

使用异或运算符来表示权限中的READ和WRITE。

接下来,在fpga_register.c中,您需要定义一个新的结构体。

typedef struct register_t2 {
    char permissions;
    void * address;
} FPGARegisterReal;

所以你需要返回指向它的指针,而不是在fpga_init中返回指向FPGARegister的指针。

接下来,在fpga_readfpga_write中,你需要检查权限并且:

  • 如果操作被允许,将参数中的FPGARegister强制转换回FPGARegisterReal,执行所需操作(设置或读取值),并返回成功代码
  • 如果操作不被允许,只需返回错误代码

这样,包括头文件在内的任何人都无法访问FPGARegisterReal结构体,因此它将无法直接访问寄存器地址。显然,有人可能会进行黑客攻击,但我相信这种故意的黑客攻击不是你真正关心的问题。


8
我曾经使用过很多硬件,其中一些有“只读”或“只写”寄存器(或者根据你对寄存器进行的读写操作有不同的功能),当有人决定执行“reg |= 4;”而不是记住它应该具有的值,设置第2位并写入新值时就会出现问题。这种情况非常棘手,因为你无法读取寄存器中随机出现和消失的位! ;) 到目前为止,我还没有见过任何实际阻止从只写寄存器读取或向只读寄存器写入的尝试。
顺便说一下,我是否已经说过,拥有“只写”寄存器是一个真正糟糕的主意,因为你不能读回以检查软件是否正确设置了寄存器,这使得调试非常困难 - 而编写驱动程序的人不喜欢通过两行VHDL或Verilog代码轻松解决的困难问题。
如果你可以控制寄存器布局,我建议你将“只读”寄存器放在4KB对齐的地址上,将“只写”寄存器放在另一个4KB对齐的地址上[超过4KB也没关系]。然后,你可以编程硬件的内存控制器来防止访问。
或者,让硬件在读取不应读取的寄存器或写入不应写入的寄存器时产生中断。我想硬件会因其他用途而产生中断吧?
使用各种C++解决方案提出的其他建议都很好,但它并不能真正阻止那些有意直接使用寄存器的人,因此如果这真的是一个安全问题(而不仅仅是“让它变得棘手”),那么你应该有硬件来保护免受硬件误用的影响。

1
这是一个很好的观点,适用于某些情况,但读回值并不总是有概念上的意义,例如当你写入一个寄存器时,它会每次附加到一个FIFO中。 - Owen
@Owen:能够读回“我上次写入该寄存器的内容”仍然是有用的。但是,我同意,在某些寄存器中这并没有太多意义。 - Mats Petersson
如果寄存器写入触发操作,那么拥有只写寄存器是一个完全不错的想法。虽然拥有只读寄存器并报告这些操作的状态可能会有所帮助,这些寄存器甚至可以与只写寄存器共享地址,但该地址实际上不会有读写寄存器,而是一个只读寄存器和一个单独的只写寄存器。 - supercat

7

在C语言中,您可以使用不完整类型的指针来防止所有的解引用:


/* writeonly.h */
typedef struct writeonly *wo_ptr_t;

/* writeonly.c */
#include "writeonly.h"

struct writeonly {
  int value 
};

/*...*/

   FOO_REGISTER->value = 42;

/* someother.c */
#include "writeonly.h"

/*...*/

   int x = FOO_REGISTER->value; /* error: deref'ing pointer to incomplete type */

只有writeonly.c,或者一般来说任何具有定义struct writeonly的代码,才能解除指针引用。当然,这段代码也可能意外地读取该值,但至少所有其他代码都被阻止了完全解除指针引用,同时能够传递这些指针并将它们存储在变量、数组和结构中。

writeonly.[ch]可以提供一个写入值的函数。


6

我认为在C语言中没有一种优雅的方式来实现它。不过,我确实看到了一种方法:

#define DEREF_PTR(type, ptr) type ptr; \
typedef char ptr ## _DEREF_PTR;

#define NO_DEREF_PTR(type, ptr) type ptr; \

#define DEREFERENCE(ptr) \
*ptr; \
{ptr ## _DEREF_PTR \
attempt_to_dereference_pointer_ ## ptr;}

int main(int argc, char *argv[]) {
    DEREF_PTR(int*, x)
    NO_DEREF_PTR(int*, y);

    DEREFERENCE(x);
    DEREFERENCE(y); // will throw an error
}

这种方法的好处在于可以提供静态错误检查。当然,使用这种方法,你需要修改所有指针声明以使用宏,这可能不是一件很有趣的事情。

编辑: 如评论中所述。

#define READABLE_PTR(type, ptr) type ptr; \
typedef char ptr ## _READABLE_PTR;

#define NON_READABLE_PTR(type, ptr) type ptr; \

#define GET(ptr) \
*ptr; \
{ptr ## _READABLE_PTR \
attempt_to_dereference_non_readable_pointer_ ## ptr;}

#define SET(ptr, value) \
*ptr = value;


int main(int argc, char *argv[]) {
    READABLE_PTR(int*, x)
    NON_READABLE_PTR(int*, y);

    SET(x, 1);
    SET(y, 1);

    int foo = GET(x);
    int bar = GET(y); // error
}

糟糕,那是正确的。我看到他提到检测取消引用,有点过于急躁了。 - Martin Svanberg
然而,同样的原则可以用于定义用于读取和设置指针后面的值的宏。 - Martin Svanberg
@MartinSvanberg 尽管如此,我想更好地理解它...您能解释一下它的预期工作方式吗? - Giacomo Tesio
2
如果您定义了一个可以解引用的指针x(DEREF_PTR(int*, x)),预处理器还会在幕后定义一个名为x_DEREF_PTR的类型。当调用DEREFERENCE时,它会在单独的作用域中实例化此类型的变量。对于使用NO_DEREF_PTR定义的指针,该类型不存在,因此会抛出错误。 - Martin Svanberg
@MartinSvanberg 在更新版本中不允许只读指针,是吗? - Giacomo Tesio
@GiacomoTesio 不,但是也可以添加这个功能。 - Martin Svanberg

0
Dan Saks有一个Youtube演示,我找不到他介绍嵌入式设备的只写寄存器的地方。
幸运的是,他写了一篇文章,更容易搜索,链接在这里:https://www.embedded.com/how-to-enforce-write-only-access/ 这是从文章中更新为C++11的代码。
class write_only_T{
public:
    write_only_T(){}
    write_only_T(T const& v) : m(v){}
    write_only_T(T&& v) : m(std::move(v)){}
    write_only_T& operator=(T const& v){
        m = v;
        return *this;
    }
    write_only_T& operator=(T&& v){
        m = std::move(v);
        return *this;
    }
    write_only_T(write_only_T const&) = delete;
    write_only_T(write_only_T&&) = delete;
    write_only_T& operator=(write_only_T const&) = delete;
    write_only_T& operator=(write_only_T&&) = delete;
private:
    T m;
};

我认为如果你使用了这个,你不需要一个特殊的指针类型,因为只写是值的属性,但我可以想象一个合成的指针类型,跳过值类型。 很可能你需要引入一个只写的引用类型等。


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