Безопасная проверка перекрывающихся областей памяти - PullRequest
0 голосов
/ 05 августа 2020

Я пытаюсь расширить свои знания и опыт в C, поэтому я пишу несколько небольших утилит.

Я копирую память, и согласно странице руководства для memcpy(3):

ПРИМЕЧАНИЯ

Несоблюдение требования о том, чтобы области памяти не перекрывались, было источником реальных ошибок. (Стандарты POSIX и C явно указывают на то, что использование memcpy () с перекрывающимися областями приводит к неопределенному поведению.) В частности, в glib c 2.13 оптимизация производительности memcpy () на некоторых платформах (включая x86-64) включала изменение порядок, в котором байты были скопированы из sr c в dest.

Очевидно, что перекрывающиеся области памяти, переданные в memcpy(3), могут вызвать множество проблем .

Я пытаюсь написать безопасную оболочку в рамках обучения C, чтобы убедиться, что эти области памяти не перекрываются:

int safe_memcpy(void *dest, void *src, size_t length);

Logi c Я пытаюсь орудие:

  • Проверьте указатели источника и назначения на наличие NULL.
  • Establi sh указателя "диапазон" для источника и назначения с параметром длины.
  • Определите, пересекается ли исходный диапазон с целевым диапазоном, и наоборот.

Моя реализация на данный момент:

#define SAFE_MEMCPY_ERR_NULL 1
#define SAFE_MEMCPY_ERR_SRC_OVERLAP 2
#define SAFE_MEMCPY_ERR_DEST_OVERLAP 3

int safe_memcpy(void *dest, void *src, size_t length) {
    if (src == NULL || dest == NULL) {
        return SAFE_MEMCPY_ERR_NULL;
    }

    void *dest_end = &dest[length - 1];
    void *src_end = &src[length - 1];

    if ((&src >= &dest && &src <= &dest_end) ||
            (&src_end >= &dest && &src_end <= &dest_end)) {
        // the start of src falls within dest..dest_end OR
        // the end of src falls within dest..dest_end
        return SAFE_MEMCPY_ERR_SRC_OVERLAP;
    }

    if ((&dest >= &src && &dest <= &src_end) ||
            (&dest_end >= &src && &dest_end <= &src_end)) {
        // the start of dest falls within src..src_end
        // the end of dest falls within src..src_end
        return SAFE_MEMCPY_ERR_DEST_OVERLAP;
    }

    // do the thing
    memcpy(dest, src, length);

    return 0;
}

Возможно, есть лучший способ сделать ошибки, но это s - это то, что у меня есть на данный момент.

Я почти уверен, что вызываю какое-то неопределенное поведение в этом коде, поскольку я нажимаю SAFE_MEMCPY_ERR_DEST_OVERLAP в областях памяти, которые не перекрываются. Когда я проверяю состояние с помощью отладчика, я вижу (например) следующие значения:

  • src: 0x7ffc0b75c5fb
  • src_end: 0x7ffc0b75c617
  • dest: 0x1d05420
  • dest_end: 0x1d0543c

Очевидно, эти адреса даже удаленно не перекрываются , следовательно почему я думаю, что запускаю UB, и предупреждения компилятора указывают как таковые:

piper.c:68:27: warning: dereferencing ‘void *’ pointer
     void *dest_end = &dest[length - 1];

Кажется , мне нужно преобразовать указатели как другой тип, но я ' m не уверен, какой тип использовать: память нетипизирована, поэтому следует ли использовать char *, чтобы «смотреть» на память как на байты? Если да, следует ли мне преобразовать все как char *? Должен ли я вместо этого использовать intptr_t или uintptr_t?

Учитывая два указателя и длину для каждого из них, как я могу безопасно проверить, перекрываются ли эти области друг друга?

Ответы [ 4 ]

4 голосов
/ 05 августа 2020

Во-первых, соответствующая программа не может выполнить арифметику c указателя на указателе типа void * или (соответственно) применить к нему оператор индексации, даже с индексом 0. void - это неполный тип , уникальный среди тех, что не может быть завершен. Наиболее важным следствием этого является то, что этот тип не передает никакой информации о размере объекта, на который он указывает, а арифметика указателя c определяется в терминах указанного объекта.

Итак да, такие выражения, как ваш &dest[length - 1], имеют неопределенное поведение по отношению к стандарту C. Некоторые реализации предоставляют расширения, влияющие на это, а другие отвергают такой код во время компиляции. В принципе, реализация может принять код и сделать с ним что-то странное, но это относительно маловероятно.

Во-вторых, вы предлагаете

написать безопасную оболочку как часть изучения C, чтобы убедиться, что эти области памяти не перекрываются

, но нет подходящего способа сделать это для общих указателей. Сравнение указателей и различия указателей определены только для указателей на один и тот же массив (или на один элемент за концом массива), где указатель на скаляр рассматривается в этом отношении как указатель на первый элемент массива размерности-1. .

Преобразование в другой тип указателя, возможно, char *, решит проблему арифметики c указателя, но не, в общем случае, проблему сопоставимости указателей. Он может получить именно то поведение, которое вы хотите от некоторых реализаций, даже надежно, но это не соответствующий подход к проблеме, и последующее поведение undefined может привести к подлинным ошибкам в других реализациях.

Относительно часто вы может статически знать, что указатели не указывают на перекрывающиеся области. В частности, если один рассматриваемый указатель является указателем на локальную переменную в области видимости или на блок памяти, выделенный текущей функцией, то обычно вы можете быть уверены, есть ли перекрытие. Для случаев, когда вы не знаете или знаете, что перекрытие определенно существует, правильный подход - использовать memmove() вместо memcpy().

3 голосов
/ 05 августа 2020

Этот «сейф» memcpy небезопасен, так как он ничего не копирует, когда этого ожидают программы. Для безопасности используйте memmove

Вы не должны использовать &src и &dest, поскольку это не начало данных или буфера, а адрес самого параметра src и dest.

То же самое с srcend и destend

2 голосов
/ 05 августа 2020

Учитывая два указателя и длину для каждого из них, как я могу безопасно проверить, перекрывают ли эти области друг друга?

<, <=, >=, > не определены, если 2 указателя не связаны с одним и тем же объектом.

Утомительный подход проверяет конечные точки одного по отношению ко всем другим элементам и использует то, что длина источника и назначения одинакова .

int safe_memcpy(void *dest, const void *src, size_t length) {
  if (length > 0) {
    unsigned char *d = dest;
    const unsigned char *s = src;
    const unsigned char *s_last = s + length - 1;

    for (size_t i = 0; i < length; i++) {
      if (s == &d[i]) return 1; // not safe
      if (s_last == &d[i]) return 1; // not safe
    }

    memcpy(dest, src, length);
  }
  return 0;
}

Если длины буферов различаются, проверьте конечные точки более коротких по адресам более длинных элементов.

должен ли я использовать все как char *

Используйте unsigned char *. mem...(), str...() ведут себя так, как если бы каждый элемент массива был unsigned char.

Для всех функций в этом подпункте каждый символ должен интерпретироваться так, как если бы он имел тип unsigned char (и поэтому все возможные представления объекта действительны и имеют различное значение). C17dr § 7.24.1 3

С редкими дополнениями, отличными от 2, unsigned char важно, чтобы избежать ловушек signed char и сохранить -0, +0 различимость. Строки останавливаются только на + 0.

Для таких функций, как int strcmp/memcmp(), unsigned char, которые используют целочисленную математику, при сравнении элементов за пределами диапазона [0...CHAR_MAX] важно возвращать результат с правильной подписью.

Даже если void * индексация была разрешена, void *dest_end = &dest[length - 1]; очень плохо, когда length == 0 как это &dest[SIZE_MAX];

&src >= &dest s / b src >= dest даже для шанса на работу.

Адреса src, dest не имеют отношения к копии, важны только их значения.

I подозреваю, что этот ошибочный код приводит к UB в другом коде OP.

Должен ли я вместо этого использовать intptr_t или uintptr_t?

Обратите внимание, что (u)intptr_t являются необязательными типами - они могут не существовать в соответствующем компиляторе.

Даже если типы существуют, математика указателей не определена как связанная с математикой целочисленных значений.

Ясно, что эти адреса даже удаленно не перекрываются, поэтому я думаю, что запускаю UB,

«Ясно», если предполагает a адреса лайнера отображаются в целые числа, что не указано в C.

1 голос
/ 05 августа 2020

Память не типизирована, поэтому следует ли мне использовать символ *, чтобы "смотреть" на память как на байты? Если да, следует ли мне преобразовать все как char *?

Используйте unsigned char*, если вам нужно разыменовать данные, или просто char*, если вы хотите увеличить / уменьшить значение указателя на счетчик байтов.

Обычно нужно делать:

void a_function_that_takes_void(void *x, void *y) {
    char *a = x;
    char *b = y;
    /* uses a and b throughout here */
}

Если да, следует ли мне преобразовать все как char *?

Да. Также часто делают:

 void_pointer = (char*)void_pointer + 1;

Следует ли мне использовать intptr_t или uintptr_t?

Можно, но это будет то же самое, что использовать char*, кроме преобразования char* в intptr_t.

как я могу безопасно проверить, перекрывают ли эти регионы друг друга?

Хорошо бы провести небольшое исследование. как реализовать проверку перекрытия memcpy в C

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...