char
. Точнее, не с самой инициализацией, а последующим использованием такого указателя. Дело было под Linux Slackware, компилятором выступал gcc
.Проблема выглядела примерно так:
#include <stdlib.h>
void myfunc (char *p)
{
*p = 'X';
}
int main ()
{
char *str0 = "Hello";
char str1[] = "Hello2";
printf("str1 => %s\n", str1);
myfunc(str1);
printf("str1 => %s\n", str1);
printf("str0 => %s\n", str0);
myfunc(str0);
printf("str0 => %s\n", str0);
return 0;
}
Если откомпилировать этот код и попытаться выполнить, то результат будет плачевным:
> gcc a.c
> ./a.out
str1 => Hello2
str1 => Xello2
str0 => Hello
Segmentation fault
Хм, странно... Что-то не помню я, чтобы такая проблема у меня раньше возникала. А в чем же причина?
Похоже, что проблема в разнице объявления и инициализации переменных
str0
и str1
. Первое, что пришло в голову, — посмотреть, во что превращается СИшный код на этапе ассемблирования:
> gcc -S a.c
> cat a.s
Смотрим, что там получилось. Ага, вот оно:
...
LC0:
.ascii "Hello\0"
LC1:
.ascii "Hello2\0"
...
movl $LC0, -12(%ebp)
movl LC1, %eax
movl %eax, -40(%ebp)
movzwl LC1+4, %eax
movw %ax, -36(%ebp)
movzbl LC1+6, %eax
movb %al, -34(%ebp)
...
При компиляции место для обеих строк резервируется в секции
.rdata
. Но при инициализации указателя str0
в память стека просто записывается адрес размещения строки "Hello" (LC0
), а при инициализации str1
в стеке резервируется нужное количество байт и туда полностью копируется содержимое второй строки (LC1
). Когда мы пытаемся изменить вторую строку (которая скопирована в стек), то она без проблем меняется. А при попытке изменить первую — мы лезем в сегмент данных. Вот тут-то нас и поджидает Seqmentation fault
. Неплохо...Немного странно, что выражение
char *str0 = "..."
в результате дает константную строку. Ну, да ладно.Интересно, что
Seqmentation fault
, как правило, возникает в системах с защитой памяти. А ну-ка, проверим, что там у нас в других системах: Solaris, gcc — то же самое; Windows, MSYS, gcc — то же... Ну, что ж, все понятно. Ура.Стоп. Раз уж я полез в другие системы, может быть и компилятор другой попробовать. Что у нас там в Windows есть? Компилятор
cl.exe
от Microsoft:
> cl.exe a.c
> a.exe
str1 => Hello2
str1 => Xello2
str0 => Hello
str0 => Xello
Ой... :-/ Это что такое? А где же "unexpected exception" или "memory access denied"?.. Неужели
cl
генерирует принципиально другой код? Ну-ка, cl
, покажи, что там у тебя в ассемблере:
...
$SG829 DB 'Hello', 00H
ORG $+2
$SG831 DB 'Hello2', 00H
ORG $+1
...
mov DWORD PTR _str0$[ebp], OFFSET FLAT:$SG829
mov eax, DWORD PTR $SG831
mov DWORD PTR _str1$[ebp], eax
mov cx, WORD PTR $SG831+4
mov WORD PTR _str1$[ebp+4], cx
mov dl, BYTE PTR $SG831+6
mov BYTE PTR _str1$[ebp+6], dl
...
Странно, но все то же самое — для первой строки в стек загоняется только указатель, а вторая полностью копируется в стек. Получается, что система тут не при чем? Все дело в компиляторе?
Пришлось лезть в MSDN и читать документацию. После недолгих поисков был найден некий ключ
/GF
, который, помимо прочего, заставляет компилятор размещать строковые константы в read-only memory. "If you try to modify strings under /GF, an application error occurs." Да, это обеспечит защиту строковых "констант" от модификации, но только если указать нужный ключ компилятора.А что же
gcc
? Внимательно просмотрев документацию, обнаружил раздел "Incompatibilities of GCC", первый пункт которого гласил: "GCC normally makes string constants read-only." Вот так. То есть и gcc
и cl
могут сгенерировать код с одинаковым поведением, но первый делает это по умолчанию, а второй — только, если указать нужную опцию.Вот тут-то и пришла в голову идея заглянуть, наконец-таки, в стандарт языка Си. Параграф 6.7.8 стандарта гласит:
"...the declarationchar *p = "abc";
definesp
with type 'pointer to char' and initializes it to point to an object with type 'array of char' with length 4 whose elements are initialized with a character string literal. If an attempt is made to use p to modify the contents of the array, the behavior is undefined."
Эээ... И что?
Читаем внимательней — "behavior is undefined". Именно эти три слова являются причиной такого поведения моего кода под разными компиляторами. "Поведение неопределено" — этого должно быть вполне достаточно для нормальных людей, чтобы не использовать конструкции вида
char *p = "abc"
.Мда... Я зачем-то провел "масштабное исследование" проблемы, которое все равно меня привело туда, куда я должен был пойти в самом начале. К Стандарту. Стыдно? Да.
В чем мораль этого рассказа? Follow the standards.
По логике, тут должна быть ошибка компиляции, но только не в Си. Ведь эдак каждый дурак сможет программировать, и что тогда делать профессионалам?
ОтветитьУдалить