Взлома макета памяти

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

#include <stdio.h>
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

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

точная лекция url начиная с 5:15

Ответ 1

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


Приложение:

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

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

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

Теперь вернемся к вашему регулярному программированию...


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

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

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


Например, этот транскрипт показывает, как 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

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

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

Ответ 2

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

Ответ 3

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

Способы его отказа:

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

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

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

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

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

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

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

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

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

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