Почему GCC ошибается, а clang не работает в учебнике?

Изучение C с помощью «Системного программирования на C и Unix» Адама Гувера. Я столкнулся с вопросом из главы 4, который меня очень озадачил. Вопрос заключается в следующем:

В следующем коде первая достигнутая функция printf() возвращает "14", но вторая функция printf() может вызвать ошибку шины или ошибку сегментации. Почему?

Оригинальный код из книги:

main()
{ 
  int *p;
  funct(p);
  printf("%d\n",*p);
}
funct(int *p2)
{
  p2=(int *)malloc(4);
  *p2=14;
  printf("%d\n",*p2);
}

Моя слегка модифицированная "отладочная" (printf все) версия:

#include <stdio.h>
#include <stdlib.h>

void funct(int *p2);

int main(){
    int *p;
    printf("main p - address: %p\n", p);

    funct(p);
    printf("main p - address: %p\n", p);
    printf("main p value: %d\n", *p);
}  

void funct(int *p2){
    printf("funct (pre malloc) p2 - address: %p\n", p2);

    p2 = (int *)malloc(4);
    printf("funct (post malloc) p2 - address: %p\n", p2);

    *p2 = 14;
    printf("funct p2 value: %d\n", *p2);
}  

Я скомпилировал этот образец, используя как gcc, так и clang (в Ubuntu Linux), и clang не выдает сбой seg для кода, который должен делать именно это. Я ломал голову над этим какое-то время и не могу представить, почему и как это происходит. Любое понимание приветствуется.

Спасибо.


person jeremy    schedule 19.09.2011    source источник
comment
Если вы компилируете на x64, p2=(int *)malloc(4); недостаточно велик.   -  person Mysticial    schedule 19.09.2011
comment
Вы выделяете память копии указателя, переданного функции, а не указателю в main(). Разыменование неинициализированного указателя — это UB. UB означает, что он может дать сбой, а может и нет.   -  person Alok Save    schedule 19.09.2011
comment
Какая часть вопросов C о переполнении стека связана с неопределенным поведением?   -  person Nemo    schedule 19.09.2011
comment
Это настоящее упражнение? Вот это да. Нужно передать адрес p (&p), если нужно изменить его в funct().   -  person Michael Foukarakis    schedule 19.09.2011
comment
Я знаю, что код ужасен. Я считаю, что смысл упражнения состоял в том, чтобы найти то, что было не так. Чего я не понимаю, так это почему этот ужасный код не seg fault при компиляции с clang. Чтобы быть совершенно ясным, я думаю, что он должен seg fault, как это происходит при компиляции с помощью gcc.   -  person jeremy    schedule 19.09.2011
comment
@jeremy - разыменование неинициализированного указателя не определено. Он не сломался из-за лязга, но это не значит, что он никогда не сломался из-за лязга. Просто вам не повезло на clang, когда код не seg fault.   -  person Mahesh    schedule 19.09.2011


Ответы (2)


int *p;
funct(p);
printf("%d\n",*p);

Это не верно. p передается по значению. Таким образом, любые изменения в функции не влияют на p в main. А разыменование неинициализированного указателя не определено.

Что вам на самом деле нужно сделать, это -

funct(&p) ; // in main

void funct( int **p ){
   *p = malloc(sizeof(int));
   // ...
}
person Mahesh    schedule 19.09.2011

Это поведение undefined и не должно приводить к сбою (или любому другому конкретному поведению). Компилятор может создавать любой код для таких случаев. Поскольку вы спросили, почему код, созданный clang, не дает сбоев, нам нужно покопаться в этом коде. Вот что выдает clang trunk при компиляции с -O3 на x86_64:

main:                               # @main
    pushq   %rbp
    movq    %rsp, %rbp              # Build stack frame
    movl    $.L.str, %edi
    movl    $14, %esi
    xorb    %al, %al                # no XMM registers used by varargs call
    callq   printf                  # printf(%edi = "%d\n", %esi = 14)
    movl    $.L.str, %edi
    xorb    %al, %al                # no XMM registers used by varargs call
    callq   printf                  # printf(%edi = "%d\n", %esi = ?)
    xorl    %eax, %eax
    popq    %rbp
    ret                             # return %eax = 0

Поскольку p не инициализировано, clang решил сегодня вообще скомпилировать выражение *p в ничто. Это законное преобразование, потому что clang может доказать, что выражение имеет неопределенное поведение. Выводимое значение — это то, что оказывается в регистре %esi во время вызова printf (на моей машине это -1). Возможно, это не то, что вы ожидали, но такова природа неопределенного поведения!

person Richard Smith    schedule 12.12.2011