Откуда берутся значения явно неопределенных переменных

Засел над простенькой программой:

#include <stdio.h>

void first()
{
    int a=15, b=72, c=54;
};

void second()
{
    int a, b, c;
    printf("%d %d %d\n", a, b, c);
};

int main()
{
    first();
    second();
}

Интерес был в том, что вывод программы всегда был:

arturko@ARTURKO-NOTE ~/homework
$ ./test.exe
15 72 54

arturko@ARTURKO-NOTE ~/homework
$ ./test.exe
15 72 54

Из листинга программы видно, что a,b,c переменные локальные и будучи определенными в одной функции как они получают те же значений во второй?

(gdb) disas first
Dump of assembler code for function first:
   0x00000001004010d0 <+0>:     push   rbp
   0x00000001004010d1 <+1>:     mov    rbp,rsp
   0x00000001004010d4 <+4>:     sub    rsp,0x10
   0x00000001004010d8 <+8>:     mov    DWORD PTR [rbp-0x4],0xf
   0x00000001004010df <+15>:    mov    DWORD PTR [rbp-0x8],0x48
   0x00000001004010e6 <+22>:    mov    DWORD PTR [rbp-0xc],0x36
   0x00000001004010ed <+29>:    add    rsp,0x10
   0x00000001004010f1 <+33>:    pop    rbp
   0x00000001004010f2 <+34>:    ret
End of assembler dump.

При вызове first() следите за указателем стэка rsp. Мы выделяем 0x10 байт для хранения трех переменных в стэке. Записываем каждую переменную через 4 байта (int занимает 4 байта) определяя её значением. Потом функция first заканчивается и мы восстанавливаем адрес верхушки стэка add rsp,0x10. В итоге у нас стэк уменьшился и не содержит информации о переменных, но память под верхушкой стэка ещё содержит в себе эту информацию.
Потом запускаем функцию second:

(gdb) disas second
Dump of assembler code for function second:
   0x00000001004010f3 <+0>:     push   rbp
   0x00000001004010f4 <+1>:     mov    rbp,rsp
   0x00000001004010f7 <+4>:     sub    rsp,0x30
   0x00000001004010fb <+8>:     mov    ecx,DWORD PTR [rbp-0xc]
   0x00000001004010fe <+11>:    mov    edx,DWORD PTR [rbp-0x8]
   0x0000000100401101 <+14>:    mov    eax,DWORD PTR [rbp-0x4]
   0x0000000100401104 <+17>:    mov    r9d,ecx
   0x0000000100401107 <+20>:    mov    r8d,edx
   0x000000010040110a <+23>:    mov    edx,eax
   0x000000010040110c <+25>:    lea    rcx,[rip+0x1f1d]        # 0x100403030
   0x0000000100401113 <+32>:    call   0x1004011c0 <printf>
   0x0000000100401118 <+37>:    nop
   0x0000000100401119 <+38>:    add    rsp,0x30
   0x000000010040111d <+42>:    pop    rbp
   0x000000010040111e <+43>:    ret
End of assembler dump.

В самом начале мы сохраняем адрес вершины стэка rsp в rbp и выделяем 0х30 байт на стэк для этой функции, а после окончания предыдущей функции rsp указывал как раз на то место в памяти стэка, под которым хранились значения переменных из прошлой функции, регистр rsp между вызовами не менялся. В итоге:

В первой функции:

0x00000001004010d8 <+8>:     mov    DWORD PTR [rbp-0x4],0xf
0x00000001004010df <+15>:    mov    DWORD PTR [rbp-0x8],0x48
0x00000001004010e6 <+22>:    mov    DWORD PTR [rbp-0xc],0x36

Мы помещаем в стэк по определенным адресам значения переменных. А во второй:

0x00000001004010fb <+8>:     mov    ecx,DWORD PTR [rbp-0xc]
0x00000001004010fe <+11>:    mov    edx,DWORD PTR [rbp-0x8]
0x0000000100401101 <+14>:    mov    eax,DWORD PTR [rbp-0x4]

Помещаем в значения регистров (которые будут использованы как наши переменные для вызова функции printf) значение переменных из первой функции которые остались в памяти и не затерлись (компилятор GCC даже не создавал отдельных переменных для этой функции, MSVC создал и присвоил те же значения).

В итоге так вот во время работы программы информация сохраняется в оперативной памяти. Мне кажется что именно ошибка в подобном коде, когда память не очищалась после функции, которая скажем считывала пароли, привела к уязвимости в OpenSSL (Heartbleed уязвимость, самая громкая ошибка этого года), которая дала возможность хакерам мониторить оперативную память (стэк) на появление в ней паролей пользователей. Но это только догадка, сопляк я ещё чтобы о таком с точностью говорить. Надеюсь, интересно было. Не только ж о вэбмордах здесь писать, люди ещё и десктоп приложения тестят )

3 лайка

На сколько я понимаю, код на С или С++.
А там нужно(и очень важно) инициализировать каждую переменную по созданию. А так же мусор собирать вручную.

В даном примере результат бЬІл таков только в рамках даной операционной системЬІ (винда, на сколько я понимаю, судя по результатам)
Если запустить под другой(например линь или мак), то результат второго запуска бЬІл бЬІ другим(и вообще непредсказуемЬІм), потому как распределение памяти идет по совсем другим алгоритмам и память скорее всего вЬІделится в другом месте. А если там за собой мусор не подчистили, то у тебя могут оказатся вообще рандомнЬІе значения =)

Есть еще анекдот на даную тему:
Буратино дали три яблока. Два он съел. Сколько яблок осталось у Буратино? Думаете одно? Ничего подобного. Никто не знает сколько у него уже было яблок до этого. Мораль — всегда обнуляйте переменные!

На самом деле и в виндовсе и в линуксе здесь поведение будет одинаковым… Более того, на всех процессорах x86 и x64 от Intel, AMD, ATT оно будет работать так же… Хотя в теории на всех видах процессоров оно будет работать так же, просто я не испытывал на процессоре супер нинтендо, у него кажется не стандартное поведение в сравнении с другими (Big Endian, у остальных Little Endian).

Во вторых - на C не нужно инициализировать каждую переменную. Их вобще можно не инициализировать. А вот указатели чистить и проверять всякий раз перед использованием важно.

Ну и в третьих: винда и линукс для процессора ведут себя абсолютно одинаково. Ну отличаются у них номера прерываний, но колличество регистров, стэк, куча, направление роста последних, размер регистров, размер аддресов памяти будет абсолютно одинаковым. Так что и поведение такой программы будет одинаковым, единственное что может поменяться, это динамические аддресса переменных… но они будут меняться и на одной и той же ОС при каждом последующем запуске программы.

[quote=“arturk, post:3, topic:5305, full:true”]
десь поведение будет одинаковым…
Более того, на всех процессорах x86 и x64 от Intel, AMD, ATT оно будет работать так же… [/quote]Своими глазами видел разницу в поведении под маком и виндою. Хотя оба интела на базе х64.

Вот именно=) Если под одной осью будет вЬІделятся память в том же месте, под другой будет вЬІделятся из разнЬІх мест и соответственно будут разнЬІе значения при каждом запуске (если не обнулять переменнЬІе)
Чем по-твоему не разное поведение? =)