strcpy () / strncpy () аварийно завершает работу на элементе структуры с дополнительным пространством, когда оптимизация включена в Unix?
при написании проекта я столкнулся со странной проблемой.
это минимальный код, который мне удалось написать, чтобы воссоздать проблему. Я намеренно сохраняю фактическую строку вместо чего-то другого, с достаточным выделенным пространством.
// #include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stddef.h> // For offsetof()
typedef struct _pack{
// The type of `c` doesn't matter as long as it's inside of a struct.
int64_t c;
} pack;
int main(){
pack *p;
char str[9] = "aaaaaaaa"; // Input
size_t len = offsetof(pack, c) + (strlen(str) + 1);
p = malloc(len);
// Version 1: crash
strcpy((char*)&(p->c), str);
// Version 2: crash
strncpy((char*)&(p->c), str, strlen(str)+1);
// Version 3: works!
memcpy((char*)&(p->c), str, strlen(str)+1);
// puts((char*)&(p->c));
free(p);
return 0;
}
приведенный выше код сбивает меня с толку:
- С
gcc/clang -O0
, иstrcpy()
иmemcpy()
работает на Linux/WSL, иputs()
ниже дает все, что я ввел. - С
clang -O0
на OSX, код падает сstrcpy()
. - С
gcc/clang -O2
или-O3
на Ubuntu / Fedora / WSL код аварии (!!) atstrcpy()
, аmemcpy()
работает хорошо. - С
gcc.exe
в Windows код хорошо работает независимо от уровня оптимизации.
также я нашел некоторые другие черты код:
- (похоже) минимальный входной сигнал для воспроизведения катастрофы-это 9 байт (включая нуль-Терминатор), или
1+sizeof(p->c)
. С этой длиной (или более длинной) авария гарантирована (Дорогой я ...). - даже если я выделяю дополнительное пространство (до 1 МБ) в
malloc()
, это не поможет. Вышеуказанное поведение не изменилось. -
strncpy()
ведет себя точно так же, даже с правильной длины поставляются в 3-м аргументом. - указатель, похоже, не имеет значения. Если член структуры
char *c
превращается вlong long c
(илиint64_t
), поведение остается прежним. (Обновление: уже изменено). -
сообщение об аварии не выглядит регулярным. Много дополнительной информации дается вместе.
я пробовал все эти компиляторы, и они не имели никакого значения:
- GCC 5.4.0 (Ubuntu / Fedora / OS X / WSL, все 64-разрядные)
- GCC 6.3.0 (только Ubuntu)
- GCC 7.2.0 (Android, norepro???) (Этот является ли GCC от c4droid также)
- Clang 5.0.0 (Ubuntu/OS X)
- MinGW GCC 6.3.0 (Windows 7/10, оба x64)
кроме того, эта пользовательская функция копирования строк, которая выглядит точно так же, как стандартная, хорошо работает с любой конфигурацией компилятора, упомянутой выше:
char* my_strcpy(char *d, const char* s){
char *r = d;
while (*s){
*(d++) = *(s++);
}
*d = '';
return r;
}
вопросы:
- почему
strcpy()
провал? Как это возможно? - почему он терпит неудачу, только если оптимизация о?
- почему
memcpy()
не зависимо от-O
уровня??
*если вы хотите обсудить о нарушении доступа к члену структуры, pleast head over здесь.
часть objdump -d
вывод аварийного исполняемого файла (на WSL):
П. С. Изначально я хотел написать структуру, последний элемент которого является указателем на динамически выделенного пространства (для строки.) Когда я пишу структуру в файл, я не могу написать указатель. Я должен написать настоящую строку. Поэтому я придумал это решение: принудительно хранить строку вместо указателя.
также, пожалуйста, не жалуйтесь о gets()
. Я не использую его в своем проекте, но только пример кода выше.
6 ответов
я воспроизвел эту проблему на своем Ubuntu 16.10, и я нашел что-то интересное.
при компиляции с gcc -O3 -o ./test ./test.c
, программа аварийно завершит работу, если входные данные длиннее 8 байт.
после некоторого реверса я обнаружил, что GCC заменил strcpy
С memcpy_chk, это видеть.
// decompile from IDA
int __cdecl main(int argc, const char **argv, const char **envp)
{
int *v3; // rbx
int v4; // edx
unsigned int v5; // eax
signed __int64 v6; // rbx
char *v7; // rax
void *v8; // r12
const char *v9; // rax
__int64 _0; // [rsp+0h] [rbp+0h]
unsigned __int64 vars408; // [rsp+408h] [rbp+408h]
vars408 = __readfsqword(0x28u);
v3 = (int *)&_0;
gets(&_0, argv, envp);
do
{
v4 = *v3;
++v3;
v5 = ~v4 & (v4 - 16843009) & 0x80808080;
}
while ( !v5 );
if ( !((unsigned __int16)~(_WORD)v4 & (unsigned __int16)(v4 - 257) & 0x8080) )
v5 >>= 16;
if ( !((unsigned __int16)~(_WORD)v4 & (unsigned __int16)(v4 - 257) & 0x8080) )
v3 = (int *)((char *)v3 + 2);
v6 = (char *)v3 - __CFADD__((_BYTE)v5, (_BYTE)v5) - 3 - (char *)&_0; // strlen
v7 = (char *)malloc(v6 + 9);
v8 = v7;
v9 = (const char *)_memcpy_chk(v7 + 8, &_0, v6 + 1, 8LL); // Forth argument is 8!!
puts(v9);
free(v8);
return 0;
}
ваш пакет struct заставляет GCC верить, что элемент c
- это ровно 8 байт.
и memcpy_chk
потерпит неудачу, если длина копирования больше чем четвертый аргумент!
Итак, есть 2 решения:
изменить структуру
-
использование параметров компиляции
-D_FORTIFY_SOURCE=0
(любитgcc test.c -O3 -D_FORTIFY_SOURCE=0 -o ./test
), чтобы отключить функции укрепления.осторожностью: это полностью отключит проверку переполнения буфера во всей программе!!
что вы делаете-это неопределенное поведение.
компилятор может предположить, что вы никогда не будете использовать больше, чем sizeof int64_t
для переменной int64_t c
. Поэтому, если вы попытаетесь написать больше, чем sizeof int64_t
(Он же sizeof c
) on c
, у вас будет проблема вне границ в вашем коде. Это так, потому что sizeof "aaaaaaaa"
>sizeof int64_t
.
дело в том, даже если вы выделяете правильный размер памяти с помощью malloc()
, компилятор может предположить, что вы никогда не будете использовать более sizeof int64_t
в своем strcpy()
или memcpy()
звонок. Потому что вы отправляете адрес c
(Он же int64_t c
).
TL; DR: вы пытаетесь скопировать 9 байтов в тип, состоящий из 8 байтов (мы предполагаем, что байт является октетом). (От @Kcvin)
если вы хотите что-то подобное, используйте гибкие члены массива из C99:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
size_t size;
char str[];
} string;
int main(void) {
char str[] = "aaaaaaaa";
size_t len_str = strlen(str);
string *p = malloc(sizeof *p + len_str + 1);
if (!p) {
return 1;
}
p->size = len_str;
strcpy(p->str, str);
puts(p->str);
strncpy(p->str, str, len_str + 1);
puts(p->str);
memcpy(p->str, str, len_str + 1);
puts(p->str);
free(p);
}
Примечание: Для стандартной цитаты, пожалуйста, обратитесь к этой ответ.
нет ответа еще подробно говорили о том, почему этот код может или не может быть неопределенным поведением.
стандарт недооценен в этой области, и есть предложение активно исправить его. В соответствии с этим предложением этот код не будет неопределенным поведением, и компиляторы, генерирующие код, который аварийно завершает работу, не будут соответствовать обновленному стандарту. (Я возвращаюсь к этому в моем заключительном пункте ниже).
но обратите внимание, что на основе обсуждения -D_FORTIFY_SOURCE=2
in другие ответы, похоже, это поведение намеренно со стороны участвующих разработчиков.
я буду говорить на основе следующего фрагмента:
char *x = malloc(9);
pack *y = (pack *)x;
char *z = (char *)&y->c;
char *w = (char *)y;
теперь все три x
z
w
обратитесь к тому же местоположению памяти и будет иметь то же значение и то же представление. Но компилятор обрабатывает z
иначе x
. (Компилятор также обрабатывает w
по-разному к одному из этих двух, хотя мы не знаем, как ОП не исследовал этот случай).
эта тема называется указатель на происхождение. Это означает ограничение, для которого значение указателя может находиться в диапазоне. Компилятор принимает z
как имеющий происхождение только за y->c
, тогда как x
имеет происхождение по всему 9-байтовому распределению.
текущий стандарт C не очень хорошо определяет происхождение. Такие правила, как вычитание указателя может происходить только между двумя указателями к тому же объекту массива пример правило провенанс. Еще одно правило происхождения относится к обсуждаемому нами коду, C 6.5.6 / 8:
когда выражение с целочисленным типом добавляется или вычитается из Указателя, результат имеет тип операнда указателя. Если операнд указателя указывает на элемент объекта array, а массив достаточно велик, результат указывает на элемент, смещенный от исходного элемента так, что разность индексов результирующего и исходного элементов массива равна целочисленному выражению. Другими словами, если выражение
P
точкиi
- й элемент объекта массива, выражения(P)+N
(что эквивалентноN+(P)
) и(P)-N
(гдеN
имеет значениеn
) указывают, соответственно,i+n
-й иi−n
- й элементы объекта array, при условии, что они существуют. Более того, если выражение P указывает на последний элемент массива объекта, выражение(P)+1
указывает на один мимо последнего элемента объекта массива, и если выражениеQ
указывает на один последний элемент объекта массива, выражение(Q)-1
указывает на последний элемент объекта array. Если и операнд указателя, и результат указывают на элементы одного и того же объекта массива или на один последний элемент объекта массива, оценка не должна вызывать переполнения; в противном случае поведение не определено. Если результат указывает один мимо последнего элемент объекта массива, он не должен использоваться в качестве операнда унарного*
оператор, который оценивается.
обоснование для проверки границ strcpy
, memcpy
всегда возвращается к этому правилу - эти функции определяются так, как если бы они были серией назначений символов из базового указателя, который увеличивается, чтобы добраться до следующего символа, а увеличение указателя покрывается (P)+1
как уже обсуждалось в этой правило.
обратите внимание, что термин "объект массива" может применяться к объекту, который не был объявлен как массив. Это прописано в 6.5.6/7:
для целей настоящих операторов, указатель на объект, который не является элементом массива ведет себя так же, как указатель на первый элемент массива длины с типом объекта, его тип элемента.
большой вопрос здесь: что такое "массив объект"? В этом коде это y->c
, *y
, или фактический 9-байтовый объект, возвращенный malloc?
важно то, что стандарт не проливает никакого света на этот вопрос. Всякий раз, когда у нас есть объекты с подобъектами, стандарт не говорит, относится ли 6.5.6/8 к объекту или подобъекту.
еще одним осложняющим фактором является то, что стандарт не содержит определения для"array", ни для "объекта массива". Но резать долго короче говоря, объект, выделенный malloc
описывается как "массив" в разных местах стандарта, поэтому кажется, что 9-байтовый объект здесь является допустимым кандидатом на "объект массива". (На самом деле это только такой кандидат для случая использования x
для итерации по 9-байтовому распределению, которое, я думаю, все согласятся, является законным).
Примечание: этот раздел очень спекулятивный, и я пытаюсь привести аргумент о том, почему решение, выбранное компиляторами здесь, не является самосогласованным
аргумент мог бы что &y->c
означает, что происхождение int64_t
подобъекта. Но это сразу же приводит к трудностям. Например,y
имеют происхождение *y
? Если так, то (char *)y
должен иметь происхождение *y
еще, но тогда это противоречит правилу 6.3.2.3 / 7, что приведение указателя на другой тип и обратно должно возвращать оригинал указатель (до тех пор, пока выравнивание не нарушается).
еще одна вещь, которую он не покрывает, - это перекрывающееся происхождение. Может ли указатель сравнивать неравный указатель с указателем того же значения, но меньшего происхождения (который является подмножеством большего происхождения) ?
далее, если мы применим тот же принцип К случае, когда подобъект является массивом:
char arr[2][2];
char *r = (char *)arr;
++r; ++r; ++r; // undefined behavior - exceeds bounds of arr[0]
arr
определяется как &arr[0]
в этом контексте, так что если происхождение &X
и X
, тогда r
фактически ограничен только первой строкой массива - возможно, удивительный результат.
можно было бы сказать, что char *r = (char *)arr;
ведет к UB здесь, но char *r = (char *)&arr;
нет. На самом деле я много лет назад продвигал этот взгляд в своих постах. Но я больше этого не делаю: в моем опыте попыток защитить эту позицию, она просто не может быть сделана самосогласованной, слишком много проблемных сценариев. И даже если бы это можно было сделать самосогласованным, факт остается фактом: стандарт этого не уточняет. В лучшем случае это мнение должно иметь статус предложения.
чтобы закончить, я бы рекомендовал читать N2090: уточнение происхождения указателя (проект отчета о дефекте или предложение для C2x).
их предложение заключается в том, что происхождение всегда относится к выделение. Это делает спорными все сложности объектов и подобъектов. Нет никаких перераспределений. В этом предложении все x
z
w
идентичны и могут использоваться для диапазона по всему 9-байтовому распределению. ИМХО простота этого привлекательна, по сравнению с тем, что обсуждалось в моем предыдущем разделе.
это все из-за -D_FORTIFY_SOURCE=2
умышленно сбой на том, что он решает небезопасно.
некоторые дистрибутивы строят gcc с -D_FORTIFY_SOURCE=2
включен по умолчанию. Некоторые-нет. Это объясняет все различия между разными компиляторами. Вероятно, те, которые не аварийно завершают работу, будут, если вы создадите свой код с помощью -O3 -D_FORTIFY_SOURCE=2
.
почему он терпит неудачу, только если оптимизация включена?
_FORTIFY_SOURCE
требуется компиляция с оптимизацией (-O
) чтобы отслеживать размеры объектов с помощью приведений / назначений указателей. См.слайды из этого разговора подробнее о _FORTIFY_SOURCE
.
почему strcpy () терпит неудачу? Как это возможно?
вызовы gcc __memcpy_chk
на strcpy
только с -D_FORTIFY_SOURCE=2
. передает 8
как размер целевого объекта, потому что он думает, что вы имеете в виду / что он может выяснить из исходного кода, который вы ему дали. То же самое для strncpy
вызов __strncpy_chk
.
__memcpy_chk
прерывание по назначению. _FORTIFY_SOURCE
может выходить за пределы вещей, которые UB в C и запрещать вещи, которые выглядят потенциально опасные. Это дает ему лицензию, чтобы решить, что ваш код небезопасен. (Как указывали другие, гибкий член массива как последний член вашей структуры и / или объединение с гибким членом массива-это то, как вы должны выразить то, что вы делаете в C.)
GCC, даже предупреждает, что проверка будет всегда терпеть неудачу:
In function 'strcpy',
inlined from 'main' at <source>:18:9:
/usr/include/x86_64-linux-gnu/bits/string3.h:110:10: warning: call to __builtin___memcpy_chk will always overflow destination buffer
return __builtin___strcpy_chk (__dest, __src, __bos (__dest));
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
(от gcc7.2 -O3 -Wall
в проводнике компилятора Godbolt).
почему
memcpy()
не зависимо от
зачем все усложнять? Overcomplexifying, как вы делаете, дает просто больше места для неопределено поведение, в этой части:
memcpy((char*)&p->c, str, strlen(str)+1);
puts((char*)&p->c);
предупреждение: передача аргумента 1 'puts' из несовместимого указателя ty pe [- Wincompatible-типы указателей] puts (&p->c);
вы явно в конечном итоге в нераспределенной области памяти или где-то записывается, если Вам ПОВЕЗЕТ...
оптимизация или нет может изменить значения адресов, и он может работать (так как адреса совпадают), или нет. Ты просто не может делать то, что вы хотите сделать (в основном ложь компилятору)
Я:
- выделите только то, что нужно для структуры, не учитывайте длину строки внутри, это бесполезно
- не используйте
gets
как это небезопасно и отживающие - использовать
strdup
вместо подверженных ошибкамmemcpy
код, который вы используете так как ты управляешь струнами.strdup
не забудем выделить нуль-Терминатор,и установим его в целевой для вас. - не забудьте освободить дублированную строку
- читать предупреждения,
put(&p->c)
неопределено поведение
ваш указатель p->c является причиной аварии.
Сначала инициализируйте структуру с размером "unsigned long long" плюс размер "*p".
Второй инициализируйте указатель p - >c требуемым размером области.
Сделать копию операции: strcpy (p->c, str);
Наконец, освободите сначала free (p - >c) и free(p).
Я думаю, дело было в этом.
[EDIT]
Я буду настаивать.
Причина ошибки заключается в том, что его структура резервирует пространство только для указателя, но не выделяет указатель для данные, которые будут скопированы.
посмотреть
int main() { pack *p; char str[1024]; gets(str); size_t len_struc = sizeof(*p) + sizeof(unsigned long long); p = malloc(len_struc); p->c = malloc(strlen(str)); strcpy(p->c, str); // This do not crashes! puts(&p->c); free(p->c); free(p); return 0; }
[EDIT2]
Это не традиционный способ хранения данных, но это работает:
pack2 *p; char str[9] = "aaaaaaaa"; // Input size_t len = sizeof(pack) + (strlen(str) + 1); p = malloc(len); // Version 1: crash strcpy((char*)p + sizeof(pack), str); free(p);