【Настоящий бой 🚀】Обучайте себя в Лаборатории бомб, прикасаясь руками

Операционная система

Рука об руку, чтобы научить вас Лаборатория бомб

Инструкции по чтению: Есть много онлайн-руководств по версии bomb с открытым исходным кодом для учебников, поддерживающих CMU.Например, две статьи моего научного руководителя написаны довольно хорошо.

Глубокое понимание компьютерных систем (CS:APP) - подробное объяснение Bomb Lab, Глубокое понимание экспериментального отчета компьютерной системы BombLab. Я также не хочу делать повторяющуюся работу, чтобы написать то, что уже написали другие. Этот блог основан на бомбовой версии, присланной мне школой. Соответствующие исполняемые файлы будут загружены на мой github после окончания эксперимента. Выше, если вы хотите использовать другую версию бомбы для практики, вы можете использовать мою.

Всего в этом эксперименте 6 этапов и скрытый этап. В данный момент я не проходил скрытый этап. Если учитель сказал, что скрытый этап добавит очков, я могу его добавить ^_^! PS: Написание блога занимает больше времени времени, чем проводить эксперимент. QwQ

Сначала перенесите экспериментальное введение выше в инструкцию:

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

«Бинарные бомбы» (бинарные бомбы, далее именуемые бомбами) — это исполняемая программа на языке C для Linux, которая содержит 6 фаз (фаза 1–фаза 6). Каждый этап операции с бомбой требует от вас ввода определенной строки. Если ваш ввод соответствует вводу, ожидаемому программой, бомба на этом этапе «обезвреживается», в противном случае бомба «взрывается» и выводит слова «БУМ!! !" . Цель эксперимента - обезвредить как можно больше уровней бомб.

Каждая фаза бомбы исследует различные аспекты программы на машинном языке с возрастающей сложностью:

* Фаза 1: сравнение строк

* Фаза 2: Цикл

* Фаза 3: Условия / ветвь

* Этап 4: рекурсивный вызов и стек

* Этап 5: Указатели

* Этап 6: связанный список/указатель/структура

Также есть скрытый этап, но он появляется только в том случае, если вы добавляете определенную строку к решению на этапе 4.

Чтобы выполнить задачу по уничтожению двоичной бомбы, вам необходимо использовать отладчик gdb и objdump для дизассемблирования исполняемого файла бомбы, а также пошаговой трассировки и отладки каждого этапа машинного кода, понимания поведения или функции каждой сборки. код языка, а затем попытаться «вывести» целевую строку, необходимую для обезвреживания бомбы. Для этого может потребоваться установить точки останова перед начальным кодом каждого этапа и перед функцией, которая взрывает бомбу, для облегчения отладки.

Экспериментальный язык: язык C

Экспериментальная среда: Linux

Затем сделайте краткое описание некоторых файлов, используемых в эксперименте:

  • бомба: исполняемая программа бомбы.

  • bomb.c: основная функция программы-бомбы.

"беги ./бомба":./bombЭто исполняемая программа, для которой требуется 0 или 1 параметр командной строки (подробности см. в функции main() в исходном файле bomb.c). Если вы не укажете параметр, то программа распечатает приветственную информацию, я жду вас, используя строку бомбы в каждой фазе, и определяет, основаны ли вы на соответствующем этапе или взрыве бомбы в соответствии с вашим миссия текущей входной строки не удалась.

Вы также можете организовать строку бомб каждой стадии в текстовый файл (например: result.txt) построчно, а затем передать ее программе в качестве единственного аргумента командной строки при запуске программы (./bomb result.txt) , программа автоматически прочитает строки в текстовом файле и по очереди проверит строки, соответствующие каждому этапу, чтобы определить успех или неудачу подрыва бомбы.

текст начинается

Сначала поместите исполняемый файлbombЧтобы разобрать:

objdump -d bomb > disassemble.asm

Затем откройте файл в своем любимом редакторе, я использовалVsCode

Вы можете установить этот плагин, чтобы выделить код

Основная функция относительно длинная, во всяком случае, я ее не видел, напрямуюCtrl + Fпоискphase_1войти в этап 1

первый этап

Фаза 1 относительно проста, и темы, которые мне были назначены, следующие:

08048b33 <phase_1>:
 8048b33:	83 ec 14             	sub    $0x14,%esp
 8048b36:	68 a4 9f 04 08       	push   $0x8049fa4
 8048b3b:	ff 74 24 1c          	pushl  0x1c(%esp)
 8048b3f:	e8 5e 04 00 00       	call   8048fa2 <strings_not_equal>
 8048b44:	83 c4 10             	add    $0x10,%esp
 8048b47:	85 c0                	test   %eax,%eax
 8048b49:	74 05                   je     8048b50 <phase_1+0x1d>
 8048b4b:	e8 49 05 00 00          call   8049099 <explode_bomb>
 8048b50:	83 c4 0c             	add    $0xc,%esp
 8048b53:	c3                   	ret   

Давайте проанализируем этот код, мы увидим, что есть две операции push, а затем есть операция вызова, так что предположим, что эти две операции push предназначены для этого.strings_not_equalПараметры передачи функций, мы можем использоватьgdbОтладьте, чтобы увидеть, так ли это:

gdb bomb
b phase_1 //打断点
b explode_bomb
run
layout asm //提供图形界面, 可以更好的观察执行到哪一条语句了

моя входная строкаhello, а затем вы можете увидеть в процессе отладки, что строка, помещенная в стек с помощью 0x1c(%esp),hello

$1 = 0x804c3e0 <input_strings> "hello"

Таким образом, мы можем предположить, что наша входная строка должна соответствовать0x8049fa4Сравните строки по этому адресу. Если они равны, бомба может быть успешно обезврежена. Теперь проверьте строки по этому адресу.

x /s 0x8049fa4

После выполнения получается следующий вывод:All your base are belong to us.правильный ответ

(gdb) x /s 0x8049fa4
0x8049fa4:      "All your base are belong to us."

В целом, эта задача достаточно проста.

второй этап

На этапе 2 проверяется цикл, и объем кода намного больше, чем на этапе 1. Сначала вставьте полный код этапа 2.

08048b54 <phase_2>:
 8048b54:	56                   	push   %esi
 8048b55:	53                   	push   %ebx
 8048b56:	83 ec 2c             	sub    $0x2c,%esp
 8048b59:	65 a1 14 00 00 00    	mov    %gs:0x14,%eax
 8048b5f:	89 44 24 24          	mov    %eax,0x24(%esp)
 8048b63:	31 c0                	xor    %eax,%eax
 8048b65:	8d 44 24 0c          	lea    0xc(%esp),%eax
 8048b69:	50                   	push   %eax
 8048b6a:	ff 74 24 3c          	pushl  0x3c(%esp)
 8048b6e:	e8 4b 05 00 00       	call   80490be <read_six_numbers>
 8048b73:	83 c4 10             	add    $0x10,%esp
 8048b76:	83 7c 24 04 01       	cmpl   $0x1,0x4(%esp)
 8048b7b:	74 05                	je     8048b82 <phase_2+0x2e>
 8048b7d:	e8 17 05 00 00       	call   8049099 <explode_bomb>
 8048b82:	8d 5c 24 04          	lea    0x4(%esp),%ebx
 8048b86:	8d 74 24 18          	lea    0x18(%esp),%esi
 8048b8a:	8b 03                	mov    (%ebx),%eax
 8048b8c:	01 c0                	add    %eax,%eax
 8048b8e:	39 43 04             	cmp    %eax,0x4(%ebx)
 8048b91:	74 05                	je     8048b98 <phase_2+0x44>
 8048b93:	e8 01 05 00 00       	call   8049099 <explode_bomb>
 8048b98:	83 c3 04             	add    $0x4,%ebx
 8048b9b:	39 f3                	cmp    %esi,%ebx
 8048b9d:	75 eb                	jne    8048b8a <phase_2+0x36>
 8048b9f:	8b 44 24 1c          	mov    0x1c(%esp),%eax
 8048ba3:	65 33 05 14 00 00 00 	xor    %gs:0x14,%eax
 8048baa:	74 05                	je     8048bb1 <phase_2+0x5d>
 8048bac:	e8 df fb ff ff       	call   8048790 <__stack_chk_fail@plt>
 8048bb1:	83 c4 24             	add    $0x24,%esp
 8048bb4:	5b                   	pop    %ebx
 8048bb5:	5e                   	pop    %esi
 8048bb6:	c3                   	ret   

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

Начиная со строки 8 ниже, в строке 8 используетсяleaЗаявление, это получается0xc(%esp)Адрес, а затем нажмите этот адрес на стек. Можно догадаться, что этот адрес используется для хранения 6 числа числа, и я точно знаю, для чего следующий pushl. Затем позвонитеread_six_numbersФункция считывает 6 чисел.

строка 13,cmpl $0x1,0x4(%esp)Этот оператор сравнивает отношение размера между первым прочитанным параметром и 1. Что касается того, почему первый параметр является первым параметром, вы можете узнать это, моделируя push-pop стека.Первый параметр здесь должен быть равен 1, иначе бомба будет взорвана Затем перейдите к строке 16, чтобы выполнить.

Строки 16 и 17 хранят указатели на первое и шестое числа в регистрах ebx и esi соответственно, откуда видно, что значение регистра esi можно рассматривать как условие завершения цикла.

Строки 18, 19 и вторая удваивают число в регистре ebx и помещают его в регистр eax, а затем сравнивают, равно ли следующее число в регистре ebx значению в регистре eax, то есть после сравнения Является ли число вдвое больше предыдущего числа, если нет, то взорвите бомбу, иначе цикл продолжается, указатель перемещается назад на одно место, пока не переместится на последнее.

Итак, окончательный ответ1 2 4 8 16 32, эта задача относительно проста

третий этап

Этап 3 исследует условия и переходы, которые относительно просты.Давайте сначала посмотрим на исходный код этапа 3.

 8048bb7:	83 ec 1c             	sub    $0x1c,%esp
 8048bba:	65 a1 14 00 00 00    	mov    %gs:0x14,%eax
 8048bc0:	89 44 24 0c          	mov    %eax,0xc(%esp)
 8048bc4:	31 c0                	xor    %eax,%eax
 8048bc6:	8d 44 24 08          	lea    0x8(%esp),%eax
 8048bca:	50                   	push   %eax
 8048bcb:	8d 44 24 08          	lea    0x8(%esp),%eax
 8048bcf:	50                   	push   %eax
 8048bd0:	68 6f a1 04 08       	push   $0x804a16f
 8048bd5:	ff 74 24 2c          	pushl  0x2c(%esp)
 8048bd9:	e8 32 fc ff ff       	call   8048810 <__isoc99_sscanf@plt>
 8048bde:	83 c4 10             	add    $0x10,%esp
 8048be1:	83 f8 01             	cmp    $0x1,%eax
 8048be4:	7f 05                	jg     8048beb <phase_3+0x34>
 8048be6:	e8 ae 04 00 00       	call   8049099 <explode_bomb>
 8048beb:	83 7c 24 04 07       	cmpl   $0x7,0x4(%esp)
 8048bf0:	77 3c                	ja     8048c2e <phase_3+0x77>
 8048bf2:	8b 44 24 04          	mov    0x4(%esp),%eax
 8048bf6:	ff 24 85 00 a0 04 08 	jmp    *0x804a000(,%eax,4)
 8048bfd:	b8 f3 01 00 00       	mov    $0x1f3,%eax
 8048c02:	eb 3b                	jmp    8048c3f <phase_3+0x88>
 8048c04:	b8 c0 00 00 00       	mov    $0xc0,%eax
 8048c09:	eb 34                	jmp    8048c3f <phase_3+0x88>
 8048c0b:	b8 57 01 00 00       	mov    $0x157,%eax
 8048c10:	eb 2d                	jmp    8048c3f <phase_3+0x88>
 8048c12:	b8 9b 01 00 00       	mov    $0x19b,%eax
 8048c17:	eb 26                	jmp    8048c3f <phase_3+0x88>
 8048c19:	b8 5c 03 00 00       	mov    $0x35c,%eax
 8048c1e:	eb 1f                	jmp    8048c3f <phase_3+0x88>
 8048c20:	b8 38 02 00 00       	mov    $0x238,%eax
 8048c25:	eb 18                	jmp    8048c3f <phase_3+0x88>
 8048c27:	b8 67 01 00 00       	mov    $0x167,%eax
 8048c2c:	eb 11                	jmp    8048c3f <phase_3+0x88>
 8048c2e:	e8 66 04 00 00       	call   8049099 <explode_bomb>
 8048c33:	b8 00 00 00 00       	mov    $0x0,%eax
 8048c38:	eb 05                	jmp    8048c3f <phase_3+0x88>
 8048c3a:	b8 83 02 00 00       	mov    $0x283,%eax
 8048c3f:	3b 44 24 08          	cmp    0x8(%esp),%eax
 8048c43:	74 05                	je     8048c4a <phase_3+0x93>
 8048c45:	e8 4f 04 00 00       	call   8049099 <explode_bomb>
 8048c4a:	8b 44 24 0c          	mov    0xc(%esp),%eax
 8048c4e:	65 33 05 14 00 00 00 	xor    %gs:0x14,%eax
 8048c55:	74 05                	je     8048c5c <phase_3+0xa5>
 8048c57:	e8 34 fb ff ff       	call   8048790 <__stack_chk_fail@plt>
 8048c5c:	83 c4 1c             	add    $0x1c,%esp
 8048c5f:	c3                   	ret   

Начиная со строки 5, две операции lea здесь, очевидно, являются передачей адресов, подобно &a, и затем используются функцией sacnf.

Обратите внимание на строку 9 здесьpush $0x804a16f, здесь нажимается адресная константа.По вызываемому позже scanf догадываемся, что это должна быть строка формата.Проверяем значение этой строки через gdb

(gdb) x /s 0x804a16f
0x804a16f:	"%d %d"

Как видно из вывода, scanf принимает два целых числа типа int.

Затем посмотрите на строку 13, там есть операция сравнения между регистром eax и 1. Как правило, регистр eax хранит возвращаемое значение функции, а возвращаемое значение функции sacnf — это количество успешно прочитанных элементов, поэтому она может видно, что количество элементов читается здесь. Если оно меньше или равно 1, бомба будет взорвана.

Затем посмотрите на строку 16, которая предназначена для сравнения отношения размера между первым прочитанным параметром и 7. Обратите внимание, что ja в следующей строке является беззнаковым сравнением, поэтому видно, что значение 1 первого параметра должно быть между 0 и 7, иначе будут детонировать бомбы

Следующая строка 18, 19, интересное место, здесь реализация типичной таблицы переходов, такой как таблица векторов прерываний, в этой идее используется переключатель, например, первое число, которое я ввожу, равно 1, затем адрес Я перехожу к тому, что нужно получить в gdb следующим образом:

(gdb) print /x *(int*)(0x804a000+4)
$1 = 0x8048bfd

Затем найдите соответствующий адрес в строках 19–36 и перейдите к нему, например, 0x8048bfd — это 20-я строка, а соответствующий код —

8048bfd:	b8 f3 01 00 00       	mov    $0x1f3,%eax
 8048c02:	eb 3b                	jmp    8048c3f <phase_3+0x88>

Таким образом, значение eax равно 0x1f3, что равно 499.

Передайте код в строке 38

 8048c3f:	3b 44 24 08          	cmp    0x8(%esp),%eax
 8048c43:	74 05                	je     8048c4a <phase_3+0x93>
 8048c45:	e8 4f 04 00 00       	call   8049099 <explode_bomb>

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

Четвертый этап

Четвертый этап требует, чтобы вы перевернули соответствующий код C, что будет намного проще решить. Я скоро переверну код C для этой проблемы, но я не ожидал, что функция scanf будет ямкой. Ее первый параметр - ваш input Второй, второй параметр - это первый введенный вами, поэтому я всегда думал, что делаю неправильный реверс.Позже, под напоминанием Че Даниу, пошаговая отладка наконец-то нашла этот жалкий BUG

Позвольте мне рассказать вам, как я изменил его и как я его отладил.

Во-первых, давайте вставим код фазы_4 следующим образом:

08048ca3 <phase_4>:
 8048ca3:	83 ec 1c             	sub    $0x1c,%esp
 8048ca6:	65 a1 14 00 00 00    	mov    %gs:0x14,%eax
 8048cac:	89 44 24 0c          	mov    %eax,0xc(%esp)
 8048cb0:	31 c0                	xor    %eax,%eax
 8048cb2:	8d 44 24 04          	lea    0x4(%esp),%eax
 8048cb6:	50                   	push   %eax
 8048cb7:	8d 44 24 0c          	lea    0xc(%esp),%eax
 8048cbb:	50                   	push   %eax
 8048cbc:	68 6f a1 04 08       	push   $0x804a16f
 8048cc1:	ff 74 24 2c          	pushl  0x2c(%esp)
 8048cc5:	e8 46 fb ff ff       	call   8048810 <__isoc99_sscanf@plt>
 8048cca:	83 c4 10             	add    $0x10,%esp
 8048ccd:	83 f8 02             	cmp    $0x2,%eax
 8048cd0:	75 0c                	jne    8048cde <phase_4+0x3b>
 8048cd2:	8b 44 24 04          	mov    0x4(%esp),%eax
 8048cd6:	83 e8 02             	sub    $0x2,%eax
 8048cd9:	83 f8 02             	cmp    $0x2,%eax
 8048cdc:	76 05                	jbe    8048ce3 <phase_4+0x40>
 8048cde:	e8 b6 03 00 00       	call   8049099 <explode_bomb>
 8048ce3:	83 ec 08             	sub    $0x8,%esp
 8048ce6:	ff 74 24 0c          	pushl  0xc(%esp)
 8048cea:	6a 09                	push   $0x9
 8048cec:	e8 6f ff ff ff       	call   8048c60 <func4>
 8048cf1:	83 c4 10             	add    $0x10,%esp
 8048cf4:	3b 44 24 08          	cmp    0x8(%esp),%eax
 8048cf8:	74 05                	je     8048cff <phase_4+0x5c>
 8048cfa:	e8 9a 03 00 00       	call   8049099 <explode_bomb>
 8048cff:	8b 44 24 0c          	mov    0xc(%esp),%eax
 8048d03:	65 33 05 14 00 00 00 	xor    %gs:0x14,%eax
 8048d0a:	74 05                	je     8048d11 <phase_4+0x6e>
 8048d0c:	e8 7f fa ff ff       	call   8048790 <__stack_chk_fail@plt>
 8048d11:	83 c4 1c             	add    $0x1c,%esp
 8048d14:	c3                   	ret   

Начиная со строки 6, вот знакомый lea, знакомый push, знакомый scanf, а затем с тем же трюком вы можете узнать, является ли строка формата%d %dИтак, мы знаем, что здесь два числа также читаются, но тот, который с низким адресом - это тот, который выталкивается сразу ...

14 строк сравнили возвращаемое значение, а не 2, взорвали бомбу и еще раз доказали два числа

Строки 16-18 сравнивают первый параметр scanf, то есть отношение размера между вторым введенным числом и 2 после вычитания 2. Здесь нам нужно обратить особое внимание на строку 19 — это jbe, которая представляет собой сравнение беззнаковых чисел, поэтому мы вводим значение второго параметра должно быть между 2 и 4.

Строки 21–23 подготавливают параметры для вызова func 4. После просмотра исходного кода func 4 мы можем знать, что параметры справа передаются первыми.

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

Давайте сначала не будем смотреть на func4, а затем посмотрим на Phase_4.Из строк с 26 по 28 мы видим, что значение первого параметра, который нам нужно ввести, равно возвращаемому значению после вызова func4, иначе бомба взорвется, поэтому , далее, обращаем func4

Сначала перейдите к исходному коду func4:

08048c60 <func4>:
 8048c60:	57                   	push   %edi
 8048c61:	56                   	push   %esi
 8048c62:	53                   	push   %ebx
 8048c63:	8b 5c 24 10          	mov    0x10(%esp),%ebx
 8048c67:	8b 7c 24 14          	mov    0x14(%esp),%edi
 8048c6b:	85 db                	test   %ebx,%ebx
 8048c6d:	7e 2b                	jle    8048c9a <func4+0x3a>
 8048c6f:	89 f8                	mov    %edi,%eax
 8048c71:	83 fb 01             	cmp    $0x1,%ebx
 8048c74:	74 29                	je     8048c9f <func4+0x3f>
 8048c76:	83 ec 08             	sub    $0x8,%esp
 8048c79:	57                   	push   %edi
 8048c7a:	8d 43 ff             	lea    -0x1(%ebx),%eax
 8048c7d:	50                   	push   %eax
 8048c7e:	e8 dd ff ff ff       	call   8048c60 <func4>
 8048c83:	83 c4 08             	add    $0x8,%esp
 8048c86:	8d 34 07             	lea    (%edi,%eax,1),%esi
 8048c89:	57                   	push   %edi
 8048c8a:	83 eb 02             	sub    $0x2,%ebx
 8048c8d:	53                   	push   %ebx
 8048c8e:	e8 cd ff ff ff       	call   8048c60 <func4>
 8048c93:	83 c4 10             	add    $0x10,%esp
 8048c96:	01 f0                	add    %esi,%eax
 8048c98:	eb 05                	jmp    8048c9f <func4+0x3f>
 8048c9a:	b8 00 00 00 00       	mov    $0x0,%eax
 8048c9f:	5b                   	pop    %ebx
 8048ca0:	5e                   	pop    %esi
 8048ca1:	5f                   	pop    %edi
 8048ca2:	c3                   	ret   

Первые несколько строк готовятся к вызову функции, а не анализируются.

Строки 5 и 6 берут параметры из стека.Если вы сами проанализируете предыдущий стек, то тут не сложно проанализировать, что ebx ставит первый параметр x1, а edi ставит второй параметр x2.

Функция 7-й и 8-й строк состоит в том, чтобы судить, равен ли x1 ≤ 0. После анализа нетрудно найти код языка C, соответствующий этим ассемблерным кодам.Примечание: регистр eax хранит возвращаемое значение функции

if (x1 <= 0) return 0;

Затем девятая строка присваивает регистру eax значение x2, а затем выносит решение о x1.Если он равен 0, то для выполнения выполняется переход к 27-й строке, поэтому мы можем обратить следующий код языка C:

if (x1 == 1) return x2;

Следующий звонокfunc4(x1 - 1, x2), Из строки 18 видно, что возвращаемое значение плюс x2 присваивается esi

Линии 19-21 готовит параметры для вызова функции Func4. После небольшого анализа вы можете знать, что следующий звонокfunc4(x1 -2, x2)

Наконец, из строки 24 видно, что возвращаемое значение второго вызова func4 добавляется к значению esi для получения окончательного возвращаемого значения, поэтому общий код обратной программы выглядит следующим образом:

int func(int x1, int x2) {
    if (x1 <= 0) return 0;
    if (x1 == 1) return x2;

    return func(x1 - 1, x2) + x1 + func(x1 - 2, x2);
}

Очень элегантный.

Конечно, если вам немного сложно понять вышеизложенное, то могу привести еще один тупой, но понятный код, мы заметили, что значение регистра eax не сохраняется во время вызова, так что можно подумать это как глобальная переменная, пусть это будет переменная eax, так что у вас есть следующая программа, которая точно соответствует уродливой программе C оператора сборки:

int eax;

void func(int x1, int x2) {
    if (x1 <= 0) {
        eax = 0;
        return;
    }
   
    eax = x2;
    if (x1 == 1) return;
    eax = x1 - 1;
    func(eax, x2);
    int temp = x2 + eax;
    x1 -= 2;
    func(x1, x2);
    eax += temp;
    return;
}

int main() {
    printf("%d", eax);
}

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

Окончательный результат 264 3

Поговорим о том, как я отлаживал, когда меня в начале стравливал scanf.

Сначала gdb заходит в отладочный интерфейс, потом бьет брейкпойнт на expire_bomb, а брейкпойнт на Phase_4, потом пошаговая отладка, а потом наблюдайте значение регистра и где он взорвался, ну я чушь говорю, нет Как для отладки таким образом?Просто посмотрите документацию по командам, связанным с gdb, не нужно запоминать их.

Этап пятый

Вы могли заметить, что у меня не было комментариев к предыдущим программам, потому что мне действительно не нужны были комментарии, но начиная с Фазы 5, я начал писать комментарии, иначе я бы не знал, что я смотрю

Впрочем, сложность пятого этапа неплохая, я прошел его менее чем за час.

Исходный код первого пятого этапа:

08048d15 <phase_5>:
 8048d15:	53                   	push   %ebx
 8048d16:	83 ec 14             	sub    $0x14,%esp
 8048d19:	8b 5c 24 1c          	mov    0x1c(%esp),%ebx
 8048d1d:	53                   	push   %ebx
 8048d1e:	e8 60 02 00 00       	call   8048f83 <string_length>
 8048d23:	83 c4 10             	add    $0x10,%esp
 8048d26:	83 f8 06             	cmp    $0x6,%eax
 8048d29:	74 05                	je     8048d30 <phase_5+0x1b>
 8048d2b:	e8 69 03 00 00       	call   8049099 <explode_bomb>
 8048d30:	89 d8                	mov    %ebx,%eax;eax指向了字符串的开始
 8048d32:	83 c3 06             	add    $0x6,%ebx;ebx现在指向了字符串的结尾的后一个,象征着结束标志
 8048d35:	b9 00 00 00 00       	mov    $0x0,%ecx
 8048d3a:	0f b6 10      loop:  	movzbl (%eax),%edx;取出了eax指向的那个字符,放到了edx中,而且是0扩展
 8048d3d:	83 e2 0f             	and    $0xf,%edx;将低四位保留,高四位清零了
 8048d40:	03 0c 95 20 a0 04 08 	add    0x804a020(,%edx,4),%ecx;这个有点switch的感觉
 8048d47:	83 c0 01             	add    $0x1,%eax
 8048d4a:	39 d8                	cmp    %ebx,%eax
 8048d4c:	75 ec                	jne    8048d3a <phase_5+0x25>
 8048d4e:	83 f9 2c             	cmp    $0x2c,%ecx;需要ecx加6次等于0x2c 10 + 10 + 10 + 10 + 2 + 2
 8048d51:	74 05                	je     8048d58 <phase_5+0x43>
 8048d53:	e8 41 03 00 00       	call   8049099 <explode_bomb>
 8048d58:	83 c4 08             	add    $0x8,%esp
 8048d5b:	5b                   	pop    %ebx
 8048d5c:	c3                   	ret   

Этот вопрос исследует указатели, и я не знаю, куда я указываю, не комментируя.

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

Строка 8, по сравнению с размером 6 eax, значение eax является возвращаемым значением функции string_length, не верьте, вы можете отладить свой собственный вид, поэтому отсюда мы можем получить важное сообщение, длина строки 6

Кстати, в строке 5 тоже есть важная информация. Этот оператор push явно подготавливает параметры для вызова функции string_length, поэтому указатель на первый символ строки хранится в регистре ebx.

Затем присваивание в строке 11 заставляет eax также указывать на начало строки.Поскольку длина строки равна 6, из строки 12 видно, что она указывает на последнюю единицу в конце строки, что, очевидно, то, что мы обычно используем.str[i] != 0знак окончания

14 строк и 15 строк комментариев очень понятно, то есть некоторая обработка символов

16-я строка - добавление ecx к значению по соответствующему адресу. Здесь снова предыдущая таблица переходов недоступна. Мы можем посмотреть значение по этому адресу в gdb.

(gdb) print (int)*(0x804a020)
$2 = 2
(gdb) print (int)*(0x804a024)
$3 = 10
(gdb) print (int)*(0x804a028)
$4 = 6
(gdb) print (int)*(0x804a02c)
$5 = 1
(gdb) print (int)*(0x804a030)
$6 = 12
(gdb) print (int)*(0x804a034)
$7 = 16
(gdb) print (int)*(0x804a038)
$8 = 9
(gdb) print (int)*(0x804a03c)
$9 = 3
(gdb) ...

Из строк с 17 по 19 мы можем знать, что нам нужно добавить всего 6 раз.

В строке 20 мы видим, что значение после добавления 6 раз равно 0x2c

Таким образом, один из возможных исходов — 10 + 10 + 10 + 10 + 2 + 2.

10 означает, что младшие четыре бита соответствующего символа равны 1, а 2 означает, что значение младших четырех битов равно 0. Это получается из соотношения между адресом таблицы переходов и соответствующим значением, поэтому возможный ответ: 111100.

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

этап шестой

Код для этого вопроса огромен, если бы не задание, я бы не делал это QwQ

08048d5d <phase_6>:
 8048d5d:	56                   	push   %esi
 8048d5e:	53                   	push   %ebx
 8048d5f:	83 ec 4c             	sub    $0x4c,%esp
 8048d62:	65 a1 14 00 00 00    	mov    %gs:0x14,%eax
 8048d68:	89 44 24 44          	mov    %eax,0x44(%esp)
 8048d6c:	31 c0                	xor    %eax,%eax
 8048d6e:	8d 44 24 14          	lea    0x14(%esp),%eax
 8048d72:	50                   	push   %eax
 8048d73:	ff 74 24 5c          	pushl  0x5c(%esp)
 8048d77:	e8 42 03 00 00       	call   80490be <read_six_numbers>
 8048d7c:	83 c4 10             	add    $0x10,%esp
 8048d7f:	be 00 00 00 00       	mov    $0x0,%esi
 8048d84:	8b 44 b4 0c          	mov    0xc(%esp,%esi,4),%eax
 8048d88:	83 e8 01             	sub    $0x1,%eax
 8048d8b:	83 f8 05             	cmp    $0x5,%eax
 8048d8e:	76 05                	jbe    8048d95 <phase_6+0x38>
 8048d90:	e8 04 03 00 00       	call   8049099 <explode_bomb>
 8048d95:	83 c6 01             	add    $0x1,%esi;这里就是限定了数的取值范围
 8048d98:	83 fe 06             	cmp    $0x6,%esi
 8048d9b:	74 1b                	je     8048db8 <phase_6+0x5b>
 8048d9d:	89 f3                	mov    %esi,%ebx
 8048d9f:	8b 44 9c 0c          	mov    0xc(%esp,%ebx,4),%eax
 8048da3:	39 44 b4 08          	cmp    %eax,0x8(%esp,%esi,4)
 8048da7:	75 05                	jne    8048dae <phase_6+0x51>
 8048da9:	e8 eb 02 00 00       	call   8049099 <explode_bomb>
 8048dae:	83 c3 01             	add    $0x1,%ebx;这里限定了数字两两不能相等
 8048db1:	83 fb 05             	cmp    $0x5,%ebx
 8048db4:	7e e9                	jle    8048d9f <phase_6+0x42>
 8048db6:	eb cc                	jmp    8048d84 <phase_6+0x27>
 8048db8:	8d 44 24 0c          	lea    0xc(%esp),%eax;指向第1个数
 8048dbc:	8d 5c 24 24          	lea    0x24(%esp),%ebx;指向第6个数的后一个,也就是end标志
 8048dc0:	b9 07 00 00 00       	mov    $0x7,%ecx
 8048dc5:	89 ca                	mov    %ecx,%edx;an = 7 - an, 后面记得还要再变回来
 8048dc7:	2b 10                	sub    (%eax),%edx
 8048dc9:	89 10                	mov    %edx,(%eax)
 8048dcb:	83 c0 04             	add    $0x4,%eax;指向下一个
 8048dce:	39 c3                	cmp    %eax,%ebx
 8048dd0:	75 f3                	jne    8048dc5 <phase_6+0x68>;;

8048dd2: bb 00 00 00 00 mov $0x0,%ebx 8048dd7: eb 16 jmp 8048def <phase_6+0x92> 8048dd9: 8b 52 08 mov 0x8(%edx),%edx;大于1跳到这里, 这里目测是链表的 p = p -> next操作 8048ddc: 83 c0 01 add $0x1,%eax 8048ddf: 39 c8 cmp %ecx,%eax 8048de1: 75 f6 jne 8048dd9 <phase_6+0x7c>

8048de3: 89 54 b4 24 mov %edx,0x24(%esp,%esi,4);小于等于1跳到这里,这里的操作就是把链表指针依次放到这6个数的后面 8048de7: 83 c3 01 add $0x1,%ebx 8048dea: 83 fb 06 cmp $0x6,%ebx 8048ded: 74 17 je 8048e06 <phase_6+0xa9>;这里的处理完后,这6个数后面的链表指针的大小1排序就对应着这6个数的排序

8048def: 89 de mov %ebx,%esi 8048df1: 8b 4c 9c 0c mov 0xc(%esp,%ebx,4),%ecx;从输入后处理完的第一个数开始遍历 8048df5: b8 01 00 00 00 mov $0x1,%eax 8048dfa: ba 3c c1 04 08 mov $0x804c13c,%edx;这个是链表的首地址 8048dff: 83 f9 01 cmp $0x1,%ecx;这个数和1进行比较 8048e02: 7f d5 jg 8048dd9 <phase_6+0x7c>;大于跳转 8048e04: eb dd jmp 8048de3 <phase_6+0x86>;小于或者等于跳转 8048e06: 8b 5c 24 24 mov 0x24(%esp),%ebx;这个值就是这6个数后面的第一个链表指针的值 8048e0a: 8d 44 24 24 lea 0x24(%esp),%eax;指向了后面第一个链表指针 8048e0e: 8d 74 24 38 lea 0x38(%esp),%esi;指向最后一个链表指针 8048e12: 89 d9 mov %ebx,%ecx 8048e14: 8b 50 04 mov 0x4(%eax),%edx;指向栈上第二个链表指针 8048e17: 89 51 08 mov %edx,0x8(%ecx);让栈上第一个链表指针指向栈上第二个链表指针 8048e1a: 83 c0 04 add $0x4,%eax 8048e1d: 89 d1 mov %edx,%ecx 8048e1f: 39 c6 cmp %eax,%esi 8048e21: 75 f1 jne 8048e14 <phase_6+0xb7>;这个循环就是让栈上这6个链表节点按栈上面的顺序建立连接关系 8048e23: c7 42 08 00 00 00 00 movl $0x0,0x8(%edx);最后一个指向null 8048e2a: be 05 00 00 00 mov $0x5,%esi 8048e2f: 8b 43 08 mov 0x8(%ebx),%eax 8048e32: 8b 00 mov (%eax),%eax;现在eax指向了第二个节点了 8048e34: 39 03 cmp %eax,(%ebx);比较这两个节点的值? 8048e36: 7d 05 jge 8048e3d <phase_6+0xe0>;前一个需要大于或者等于后面一个 8048e38: e8 5c 02 00 00 call 8049099 <explode_bomb> 8048e3d: 8b 5b 08 mov 0x8(%ebx),%ebx 8048e40: 83 ee 01 sub $0x1,%esi 8048e43: 75 ea jne 8048e2f <phase_6+0xd2> 8048e45: 8b 44 24 3c mov 0x3c(%esp),%eax 8048e49: 65 33 05 14 00 00 00 xor %gs:0x14,%eax 8048e50: 74 05 je 8048e57 <phase_6+0xfa> 8048e52: e8 39 f9 ff ff call 8048790 <__stack_chk_fail@plt> 8048e57: 83 c4 44 add $0x44,%esp 8048e5a: 5b pop %ebx 8048e5b: 5e pop %esi 8048e5c: c3 ret

Я немного устал писать, не хочу вдаваться в подробности, и я написал много комментариев.Обратите внимание, что связанный список, упомянутый в комментариях, относится к той части стека, которая подключена к 6 числа.

Начиная со строки 8, здесь знакомая операция lea, знакомые операции push и call, эта функция считывает шесть чисел по указанному адресу в стеке.

Последний использует много кода — это две очень скучные операции.Во-первых, диапазон значений, определяющих 6 чисел, должен быть 1 ~ 6, а во-вторых, шесть чисел разные.

Затем выполните преобразование

Строка 56 отмечает, что есть операция присвоения адресной константы.Это значение адреса является адресом, по которому находится голова связанного списка.Через gdb мы можем увидеть следующую информацию

Каждый узел занимает 12 байт, первый байт — это значение, второй байт — номер идентификатора, а третий байт — адрес следующего узла.

Далее выполняется операция размещения узлов связного списка на стеке в определенном порядке после 6 номеров, а затем выполняется операция установления отношения связи связного списка на стеке, т. е. следующий узел каждого узла переопределяется.Кто (он последний в стеке), в конце находится основная часть информации, то есть порядок этих 6 узлов от большого к меньшему, поэтому мы можем узнать идентификатор узла в соответствии со значением узла с помощью приведенного выше рисунка. Порядок от большого к меньшему равен 6 2 4 3 1 5, и, наконец, выполнить операцию сокращения an = 7 - an, чтобы получить окончательный ответ 1 5 3 4 6 2!

Заканчиваем цветочную композицию!!!