Почему этот код c ниже работает нормально? (C Basic, тест на переполнение буфера)

#include <stdio.h>

int main(int argc, char *argv[])
{
    int arr[5] = {1, 2, 3, 4, 5};
    arr[6] = 7; // [1, 2, 3, 4, 5, 6]
    
    printf("arr[6] = %d\n", arr[6]);

    return 0;
}

Насколько я помню, раньше при компиляции выдавалось предупреждающее сообщение, а при запуске — неизвестное значение. (Или я мог видеть сообщение о неисправности сегмента).

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

Моя текущая версия GCC выглядит следующим образом:

gcc-13 (Homebrew GCC 13.2.0) 13.2.0
Copyright (C) 2023 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

Неопределённое поведение не определено. Segfault не гарантируется, равно как и печать каких-либо «неизвестных значений».

wohlstad 01.09.2024 10:59

Примечание: комментарий в вашем коде неверен. arr[6] = 7; // [1, 2, 3, 4, 5, 6] — вы назначаете 7 седьмому элементу, поэтому [1, 2, 3, 4, 5, indeterminate, 7] было бы более подходящим (если бы в коде было определено поведение)

Ted Lyngmo 01.09.2024 11:39

Этот вопрос похож на: Насколько опасно получать доступ к массиву за пределами его границ?. Если вы считаете, что это другое, отредактируйте вопрос, поясните, чем он отличается и/или как ответы на этот вопрос не помогают решить вашу проблему.

BoP 01.09.2024 11:41

Тест: замените определение arr на int x=1, arr[5] = {1, 2, 3, 4, 5}, y=2;. Добавьте printf("x=%d, y=%d\n",x,y); внизу. Если запустить программу, я получу x=1, y=7. Итак, вы знаете, где хранится arr[6].

Wiimm 01.09.2024 12:46

@Wiimm — В компиляторе, который я использую, значение нигде не сохраняется. Он просто оптимизирует массив и делает printf("arr[6] = %d\n", 7);.

BoP 01.09.2024 12:51

Спасибо всем ответили! Я понимаю, что мой вопрос был неточным, и хорошие ответы более чем компенсировали мою неясность!

Kwang-min Kim 02.09.2024 07:42
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
6
118
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

Вы используете слово «правильно работать» неправильно. Чтобы что-то работало правильно, должно быть определение правильного поведения, но его нет, и код имеет неопределенное поведение во всех версиях.

Это означает, что программа может выйти из строя, напечатать «привет, мир», зависнуть или сделать что угодно, включая печать arr[6] = 7, что не более правильно, чем любое другое поведение.

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

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

Повреждение памяти может привести ко многим вещам, а не только к "немедленному сбою сегмента", а не к немедленному сбою - это не "программа работает нормально :) :)"

Возьмем, например. программа

#include <stdio.h>

void bad_func(int index) {
    int arr[5] = {1, 2, 3, 4, 5};
    arr[index] = 7;
    printf("arr[%d] = %d\n", index, arr[index]);
    return;
}
int main(int argc, char *argv[])
{
    for(int i = 0; i < 9999; i++) {
        bad_func(i),
        printf("..survived, phew!\n");
    }
    return 0;
}

скомпилировано с помощью gcc -o crash crash.c. Это означает, что мы медленно увеличиваем индекс, при котором происходит повреждение памяти. На моей машине с Ubuntu с gcc 12.3.0 я получаю:

$ ./crash
arr[0] = 7
..survived, phew!
arr[1] = 7
..survived, phew!
arr[2] = 7
..survived, phew!
arr[3] = 7
..survived, phew!
arr[4] = 7
..survived, phew!
arr[5] = 7
..survived, phew!
arr[6] = 7
*** stack smashing detected ***: terminated
Aborted (core dumped)

В Windows с GCC 14.1.0 я получаю

>crash.exe
arr[0] = 7
..survived, phew!
arr[1] = 7
..survived, phew!
arr[2] = 7
..survived, phew!
arr[3] = 7
..survived, phew!
arr[4] = 7
..survived, phew!
arr[5] = 7
..survived, phew!
arr[6] = 7
..survived, phew!
arr[7] = 7
..survived, phew!
arr[8] = 7
..survived, phew
<crash>

Таким образом, в зависимости от компилятора и явного расположения памяти, SEGFAULT происходит по определенным индексам.

Как было сказано ранее, сбой SEGFAULT — это лишь одно из возможных последствий записи из-за нехватки памяти, которую делает ваша программа. Рассмотрим программу:

#include <stdio.h>
#include <string.h>

void bad_func(int index) {
    int arr[5] = {1, 2, 3, 4, 5};
    int my_variable = 0;
    arr[index] = 7;
    printf("arr[%d] = %d\n", index, arr[index]);
    if (my_variable) {
      printf("my_variable suddenly changed!! %d\n", my_variable);
    }
    return;
}
int main(int argc, char *argv[])
{
    for(int i = 0; i < 9999; i++) {
        bad_func(i),
        printf("..survived, phew!\n");
    }
    return 0;
}

при компиляции в моей системе Linux с помощью gcc -O0 -o crash -fno-stack-protector crash.c я получаю

arr[0] = 7
..survived, phew!
arr[1] = 7
..survived, phew!
arr[2] = 7
..survived, phew!
arr[3] = 7
..survived, phew!
arr[4] = 7
..survived, phew!
arr[5] = 7
..survived, phew!
arr[6] = 7
..survived, phew!
arr[7] = 7
my_variable suddenly changed!! 7
..survived, phew!
arr[8] = 7
..survived, phew!
Memory access error (core dumped)

В этом конкретном случае расположения памяти доступ arr[7] фактически был эквивалентен перезаписи переменной int my_variable. И это сказалось на дальнейшем ходе программы.

Насколько я помню, раньше при компиляции выдавалось предупреждающее сообщение, а при запуске — неизвестное значение. (Или я мог видеть сообщение о неисправности сегмента).

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

Тестирование каждой версии GCC, доступной для x86-64 в Compiler Explorer, показывает, что ни одна из них не выдает диагностическое сообщение для этого доступа к массиву за пределами границ. Поэтому вряд ли ваши воспоминания верны.

Возможности включают в себя:

  • Существует некоторая комбинация опций, которая создает сообщение. (Я тестировал с включенной оптимизацией, которая включает в себя анализ, который иногда позволяет проводить диагностику, которую без нее невозможно обнаружить.)
  • Ваши воспоминания относятся к более чем десятилетней давности, до GCC 4.7.3.
  • Это сообщение было выдано какой-то промежуточной версией GCC, не сохраненной в Compiler Explorer или для другой цели.
  • Это был другой компилятор, например Clang, который действительно выдаёт диагностику.

Другие вопросы по теме