Тип указателя только для записи
Я пишу программное обеспечение для встроенной системы.
Мы используем указатели для доступа к регистрам устройства FPGA.
Некоторые из регистров доступны только для чтения, а другие только для записи.
Регистры только для записи будут давать неопределенные значения при чтении.
Я хочу определить тип указателя, который позволит компилятору обнаруживать при чтении значений из регистров только для записи (иначе разыменование).
Можно ли создать указатель только для записи, используя только синтаксис языка Си?
(Мы разрабатываем первый прототип с использованием C, но переходим на C++ во 2-м поколении.)
Как создать эффективный указатель только для записи в C++? (Помните, что это не отслеживание элементов в динамической памяти, а доступ к аппаратным адресам.)
Этот код используется во встроенной системе, где безопасность и качество являются главными задачами.
6 ответов
Я, вероятно, написал бы крошечный класс-обертку для каждого:
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
Я работал с большим количеством аппаратного обеспечения, и у некоторых из них есть регистры "только для чтения" или "только для записи" (или различные функции в зависимости от того, читаете ли вы или пишете в регистр, что очень интересно, когда кто-то решит это сделать "reg |= 4;"вместо того, чтобы запоминать значение, которое оно должно иметь, установите бит 2 и запишите новое значение, как вы должны. Ничто не сравнится с попыткой отладки оборудования, в котором случайные биты появляются и исчезают из регистров, которые вы не можете прочитать!; Я до сих пор не видел никаких попыток фактически заблокировать чтение из регистра только для записи или записи в регистры только для чтения.
Кстати, я говорил, что иметь регистры, которые являются "только для записи", ДЕЙСТВИТЕЛЬНО плохая идея, потому что вы не можете прочитать обратно, чтобы проверить, правильно ли программа установила регистр, что делает отладку действительно сложной - и люди пишут драйверы не люблю отлаживать сложные проблемы, которые можно было бы действительно легко сделать с помощью двух строк кода VHDL или Verilog.
Если у вас есть некоторый контроль над компоновкой регистров, я бы посоветовал поместить регистры "только для чтения" в адрес с выравниванием 4 КБ, а регистры "только для записи" в другой адрес с выравниванием 4 КБ [более 4 КБ - это нормально]. Затем вы можете запрограммировать аппаратный контроллер памяти для предотвращения доступа.
Или, пусть аппаратное обеспечение создает прерывание, если регистры, которые не должны быть прочитаны, читаются, или регистры, которые не должны быть записаны, записываются. Я предполагаю, что оборудование производит прерывания для других целей?
Другие предложения, сделанные с использованием различных решений C++, хороши, но на самом деле это не останавливает того, кто намерен использовать регистры напрямую, поэтому, если это действительно проблема безопасности (а не "давайте сделаем это неловким"), тогда вы должны иметь оборудование для защиты от неправильного использования оборудования.
Я бы использовал комбинацию структур, чтобы представить регистр и пару функций для их обработки.
В 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 в XOR, чтобы выразить разрешения.
Чем в fpga_register.c
вы бы определили новую структуру
typedef struct register_t2 {
char permissions;
void * address;
} FPGARegisterReal;
так что вы возвращаете указатель на него вместо указателя на FPGARegister
на fpga_init
,
Затем на fpga_read
а также fpga_write
вы проверяете разрешения и
- если операция разрешена, отбросьте
FPGARegister
от аргумента доFPGARegisterReal
выполнить желаемое действие (установить или прочитать значение) и вернуть код успеха - если операция не разрешена, просто верните код ошибки
Таким образом, никто, включая заголовочный файл, не сможет получить доступ к FPGARegisterReal
структура, и, следовательно, он не будет иметь прямой доступ к адресу регистра. Очевидно, что это можно взломать, но я совершенно уверен, что такие преднамеренные взломы не являются вашими настоящими проблемами.
В 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]
может предоставить функцию для записи значения.
Я не вижу элегантного способа сделать это в 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
}
У Дэна Сакса есть презентация на 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;
};
Я не думаю, что вам нужен специальный тип указателя, если вы использовали это, потому что только для записи является свойством значения, но я могу представить синтетический тип указателя, который пропускает тип значения. Вероятно, вам нужно будет ввести ссылочный тип только для записи и так далее.