функция возвращает адрес локальной переменной, но по-прежнему компилируется в c, почему? - PullRequest
0 голосов
/ 06 августа 2020

Даже если я получаю предупреждение функция возвращает адрес из локальной переменной, она компилируется. Разве это не UB компилятора? Сгенерированная сборка:

    .text
.LC0:
    .asciz "%i\n"
    .globl  foo
    .type   foo, @function
foo:
    pushq   %rbp    #
    movq    %rsp, %rbp  #,
    sub     $16, %rsp   #,
    mov     %rdi, -8(%rbp)   #,
    leaq    -8(%rbp), %rax   #,
# a.c:5: }
    leave 
    ret
    .size   foo, .-foo
    .globl  main
    .type   main, @function
main:
    pushq   %rbp    #
    movq    %rsp, %rbp  #,
# a.c:8:    foo();
    movl    $123, %edi  #,
    call    foo #
    movq    (%rax), %rsi   #,
    leaq    .LC0(%rip), %rdi   #,
    movl    $0, %eax   #,
    call    printf #,
    movl $0, %eax
# a.c:9: }
    popq    %rbp    #
    ret 
    .size   main, .-main
    .ident  "GCC: (Debian 8.3.0-6) 8.3.0"
    .section    .note.GNU-stack,"",@progbits

Здесь assmebly возвращает адрес локальной переменной leaq -8(%rbp), %rax, но затем вызывает инструкцию leave, которая должна «аннулировать» адрес -8(%rbp) (указатель стека добавлен, поэтому я больше не сможет разыменовать этот адрес, поскольку программа продолжила работу). Так зачем же компилировать и, к счастью, разыменовать mov (%rax), %rdi, когда адрес, возвращенный на %rax, больше не действителен? Разве это не должно быть сегментировано или завершено?

Ответы [ 4 ]

6 голосов
/ 06 августа 2020

Даже если я получаю предупреждение, функция возвращает адрес из локальной переменной, она компилируется. Разве это не UB компилятора?

Нет, но если бы это было так, как бы вы узнали? Похоже, вы неправильно понимаете неопределенное поведение. Это не означает «компилятор должен отклонить это», «компилятор должен предупредить об этом», «программа должна завершиться» или что-то в этом роде. Эти могут быть проявлениями UB, но если спецификация языка требует такого поведения, то это не будет undefined . Обеспечение того, чтобы программа C не проявляла неопределенного поведения, является обязанностью программиста, а не реализации C. Если программист не выполняет эту ответственность, реализация C явно не имеет взаимных ответственность - он может делать все, что в его силах.

Более того, нет единого «своего» * ​​1064 * компилятора. Различные компиляторы могут действовать по-разному и при этом соответствовать спецификациям языка C. Вот здесь-то и появляются варианты поведения, определяемые реализацией, неопределенные и неопределенные. Разрешить такую ​​вариативность намеренно разработчики языка C. Среди прочего, это позволяет реализациям работать способами, которые естественны для их конкретного целевого оборудования и среды исполнения.

Теперь давайте go вернемся к «нет». Вот типичный пример функции, возвращающей адрес автоматической c переменной:

int *foo() {
    int bar = 0;
    return &bar;
}

Что насчет того, что должно иметь неопределенное поведение? Для функции четко определено вычисление адреса bar, и результирующее значение указателя имеет правильный тип, который должен быть возвращен функцией. После того, как время жизни bar заканчивается, когда функция возвращается, возвращаемое значение становится неопределенным (параграф 6.2.4 / 2 стандарта), но это само по себе не приводит к какому-либо неопределенному поведению.

Или рассмотрим вызывающего:

void test1() {
    int *bar_ptr = foo();  // OK under all circumstances
}

Как уже обсуждалось, возвращаемое значение нашего конкретного foo() всегда будет неопределенным, поэтому, в частности, это может быть представление ловушки. Но это соображение во время выполнения, а не во время компиляции. И даже если значение было представлением ловушки, C не требует, чтобы реализация отказала или не сохранила его. В частности, сноска 50 к C11 является явной по этому поводу:

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

Обратите внимание также, что foo() и test1() могут быть скомпилированы разными запусками компилятора, так что при компиляции test1() компилятор знает ничего о поведении foo(), кроме того, что указано его прототипом. C не предъявляет требований к времени трансляции для реализаций, которые зависят от поведения программ во время выполнения.

С другой стороны, требования к представлениям прерываний будут применяться по-другому, если бы функция была немного изменена:

void test2() {
    int *bar_ptr = NULL;
    bar_ptr = foo();      // UB (only) if foo() returns a trap representation
}

Если возвращаемое значение foo() оказывается представлением ловушки, тогда сохраняет его в bar_ptr (в отличие от инициализации bar_ptr с it) производит неопределенное поведение во время выполнения. Опять же, «undefined» означает именно то, что написано на банке. C не определяет какое-либо конкретное поведение для реализаций, которое должно проявляться в данных обстоятельствах, и, в частности, не требует, чтобы программы завершали работу или вообще проявляли какое-либо внешне видимое поведение. И снова это соображение во время выполнения, а не во время компиляции.

Более того, если возвращаемое значение foo() оказывается не представлением ловушки (вместо этого является значением указателя, которое не является адресом любого живого объекта), то нет ничего плохого в чтении самого этого значения:

void test3() {
    int *bar_ptr = foo();
    // UB (only) if foo() returned a trap representation:
    printf("foo() returned %p\n", (void *) bar_ptr);
}

Самым большим и наиболее часто используемым неопределенным поведением в этой области будет попытка разыменовать возвращаемое значение foo(), которое, вне зависимости от того, является ли представление ловушкой или нет, почти наверняка не указывает на живой объект int:

void test4() {
    int *bar_ptr = foo();
    // UB under all circumstances for the given foo():
    printf("foo() returned a pointer to an int with value %d\n", *bar_ptr);
}

Но опять же, это соображение во время выполнения, а не во время компиляции. И снова undefined означает undefined. Следует ожидать, что реализация C преобразует это успешно, пока есть объявления в области видимости для задействованных функций, и хотя некоторые компиляторы могут предупреждать, они не обязаны это делать. Поведение функции test4 во время выполнения не определено, но это не означает, что программа обязательно завершится сбоем или завершится каким-либо другим образом. Возможно, но я ожидаю, что на практике неопределенное поведение, проявляющееся во многих реализациях, будет заключаться в том, чтобы напечатать "foo () вернула указатель на int со значением 0". Это никоим образом не противоречит требованиям C.

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

Как вы заявили, если вы возвращаете адрес локальной переменной из функции и пытаетесь разыменовать (или даже читать) этот адрес, вы вызываете поведение undefined .

Формальный определение неопределенного поведения изложено в разделе 3.4.3 C стандартного :

поведения при использовании непереносимой или ошибочной конструкции программы или ошибочных данных, для которых данный международный стандарт не налагает требований

Когда происходит неопределенное поведение, компилятор не дает никаких гарантий относительно того, что произойдет. Программа может взломать sh, может выдать странные результаты или может показаться, что работает правильно.

Вообще говоря, компиляторы предполагают, что код не содержит неопределенного поведения, и работают в соответствии с этим предположением. Поэтому, когда это происходит, все ставки отменяются.

Просто потому, что программа могла cra sh, не означает, что будет .

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

Конечно, он будет компилироваться, и некоторые компиляторы выдадут диагностическое сообщение c, информирующее вас о проблеме. Многие компиляторы позволяют рассматривать такие сообщения (обычно называемые предупреждениями) как ошибки, передавая параметры командной строки.

UB означает, что поведение вашей программы при ее запуске будет undefined

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

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

(*) Согласно правилу одной программы, компилятор, который может правильно обработать по крайней мере одну программу, которая выполняет ограничения на перевод, указанные в Стандарте, могут делать все, что угодно, при подаче любого другого исходного текста. Таким образом, если бы разработчик компилятора считал более полезным отклонить все программы, удовлетворяющие некоторым критериям, несмотря на то, что некоторые такие программы были строго согласованными, чем обрабатывать такие программы, такое поведение не сделало бы компилятор несоответствующим. Тем не менее, в Стандарте в другом месте говорится, что программа будет вызывать UB, когда заданы некоторые входные данные, может быть правильной программой с полностью определенным поведением при задании других входных данных.

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