взлом расположения памяти

я проходил этот курс в YouTube, и он говорил о том, как некоторые программисты могут использовать там знание того, как память положена, чтобы сделать умные вещи.. один из примеров в лекции был чем-то как этот

#include 
void makeArray();
void printArray();
int main(){
        makeArray();
        printArray();
        return 0;
}
void makeArray(){
    int array[10];
    int i;
    for(i=0;i<10;i++)
        array[i]=i;
}
void printArray(){
    int array[10];
    int i;  
    for(i=0;i<10;i++)
        printf("%d\n",array[i]);
}

идея - целый, эти две функции имеют тот же размер записи активации на сегменте стека, это будет работать и печатать числа от 0 до 9..., но на самом деле это печатает что-то как этот

134520820
-1079626712
0
1
2
3
4
5
6
7

всегда существуют те два значения при просьбе... кто-либо может объяснить это??? Я использую gcc в Linux

точный URL лекции, запускающийся в 5:15

11
задан AstroCB 30 August 2014 в 20:44
поделиться

3 ответа

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


Добавление:

Или, иногда, только иногда, очень умные. Посмотрев видео, на которое дана ссылка в обновлении вопроса, можно сказать, что это не была какая-то обезьяна, нарушающая правила. Этот парень прекрасно понимал, что он делает.

Это требует глубокого понимания сгенерированного кода и может легко сломаться (как упоминалось и наблюдалось здесь), если изменится ваше окружение (например, компиляторы, архитектуры и так далее).

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

Теперь вернемся к обычному программированию...


Он не переносится между архитектурами, компиляторами, выпусками компиляторов и, возможно, даже уровнями оптимизации в одном и том же выпуске компилятора, а также имеет неопределенное поведение (чтение неинициализированных переменных).

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

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


Например, эта расшифровка показывает, как gcc может вести себя по-разному на разных уровнях оптимизации:

pax> gcc -o qq qq.c ; ./qq
0
1
2
3
4
5
6
7
8
9

pax> gcc -O3 -o qq qq.c ; ./qq
1628373048
1629343944
1629097166
2280872
2281480
0
0
0
1629542238
1629542245

На высоком уровне оптимизации gcc (который я предпочитаю называть безумным уровнем оптимизации) это функция makeArray. По сути, она поняла, что массив не используется, и поэтому оптимизировала его инициализацию.

_makeArray:
        pushl   %ebp            ; stack frame setup
        movl    %esp, %ebp

                                ; heavily optimised function

        popl    %ebp            ; stack frame tear-down

        ret                     ; and return

На самом деле я немного удивлен, что gcc вообще оставил там заглушку функции.

Обновление: как отмечает Николас Найт в комментарии, функция осталась, поскольку она должна быть видна компоновщику - если сделать функцию статической, то gcc удалит и заглушку.

Если вы проверите ассемблерный код на уровне оптимизации 0 ниже, это даст подсказку (это не является фактической причиной - см. ниже). Изучите следующий код и вы увидите, что фрейм стека настроен по-разному для двух функций, несмотря на то, что они имеют одинаковые передаваемые параметры и одинаковые локальные переменные:

subl    $48, %esp     ; in makeArray
subl    $56, %esp     ; in printArray

Это потому, что printArray выделяет дополнительное пространство для хранения адреса строки формата printf и адреса элемента массива, по четыре байта, что и объясняет разницу в восемь байт (два 32-битных значения).

Это наиболее вероятное объяснение того, что ваш массив в printArray() отличается на два значения.

Вот две функции на уровне оптимизации 0 для вашего удовольствия :-)

_makeArray:
        pushl   %ebp                     ; stack fram setup
        movl    %esp, %ebp
        subl    $48, %esp
        movl    $0, -4(%ebp)             ; i = 0
        jmp     L4                       ; start loop
L5:
        movl    -4(%ebp), %edx
        movl    -4(%ebp), %eax
        movl    %eax, -44(%ebp,%edx,4)   ; array[i] = i
        addl    $1, -4(%ebp)             ; i++
L4:
        cmpl    $9, -4(%ebp)             ; for all i up to and including 9
        jle     L5                       ; continue loop
        leave
        ret
        .section .rdata,"dr"
LC0:
        .ascii "%d\12\0"                 ; format string for printf
        .text

_printArray:
        pushl   %ebp                     ; stack frame setup
        movl    %esp, %ebp
        subl    $56, %esp
        movl    $0, -4(%ebp)             ; i = 0
        jmp     L8                       ; start loop
L9:
        movl    -4(%ebp), %eax           ; get i
        movl    -44(%ebp,%eax,4), %eax   ; get array[i]
        movl    %eax, 4(%esp)            ; store array[i] for printf
        movl    $LC0, (%esp)             ; store format string
        call    _printf                  ; make the call
        addl    $1, -4(%ebp)             ; i++
L8:
        cmpl    $9, -4(%ebp)             ; for all i up to and including 9
        jle     L9                       ; continue loop
        leave
        ret

Обновление: Как указывает Roddy в комментарии. это не является причиной вашей конкретной проблемы, поскольку в данном случае массив находится в одной и той же позиции в памяти (%ebp-44 с %ebp, одинаковым при двух вызовах). Я пытался указать на то, что две функции с одинаковым списком аргументов и одинаковыми локальными параметрами не обязательно будут иметь одинаковое расположение стекового кадра.

Достаточно printArray поменять местами расположение своих локальных переменных (включая любые временные параметры, не явно созданные разработчиком), и вы получите эту проблему.

23
ответ дан 3 December 2019 в 02:40
поделиться

Вероятно, GCC генерирует код, который не помещает аргументы в стек при вызове функции, а вместо этого выделяет дополнительное пространство в стеке. Аргументы вашего вызова функции 'printf', «% d \ n» и array [i] занимают 8 байтов в стеке, первый аргумент является указателем, а второй - целым числом. Это объясняет, почему есть два целых числа, которые печатаются неправильно.

4
ответ дан 3 December 2019 в 02:40
поделиться

Никогда, никогда, никогда, никогда, никогда, никогда не делайте ничего подобного. Это не будет работать надежно. Вы получите странные ошибки. Это далеко не портативно.

Пути, по которым это может не сработать:

.1. Компилятор добавляет дополнительный, скрытый код

DevStudio в режиме отладки добавляет вызовы функций, которые проверяют стек, чтобы поймать ошибки стека. Эти вызовы перезаписывают то, что было в стеке, тем самым теряя ваши данные.

.2. Кто-то добавляет вызов Enter/Exit

Некоторые компиляторы позволяют программисту определять функции, которые будут вызываться при входе в функцию и выходе из функции. Как и (1), они используют пространство стека и будут перезаписывать то, что там уже есть, теряя данные.

.3. Прерывания

В main(), если вы получите прерывание между вызовами makeArray и printArray, вы потеряете свои данные. Первое, что происходит при обработке прерывания, - это сохранение состояния процессора. Обычно это включает в себя перемещение регистров и флагов процессора в стек, и да, вы угадали, перезапись ваших данных.

.4. Компиляторы умны

Как вы видели, массив в makeArray находится по другому адресу, чем в printArray. Компилятор поместил свои локальные переменные в разные позиции на стеке. Он использует сложный алгоритм, чтобы решить, куда поместить переменную - в стек, в регистр и т.д., и не стоит пытаться понять, как компилятор это делает, поскольку следующая версия компилятора может сделать это по-другому.

Подводя итог, можно сказать, что подобные "умные трюки" не являются трюками и уж точно не умны. Вы ничего не потеряете, если объявите массив в main и передадите ссылку/указатель на него в двух функциях. Стеки предназначены для хранения локальных переменных и адресов возврата функций. Как только ваши данные выходят за пределы области видимости (т.е. вершина стека уменьшается по сравнению с данными), данные фактически теряются - с ними может произойти все, что угодно.

Чтобы проиллюстрировать этот момент, ваши результаты, вероятно, были бы другими, если бы у вас были другие имена функций (я просто предполагаю здесь, хорошо).

4
ответ дан 3 December 2019 в 02:40
поделиться
Другие вопросы по тегам:

Похожие вопросы: