Я думаю, что я отклонюсь от того, что Питер обсуждал (он предоставляет хорошую информацию), и расскажу о некоторых проблемах, которые, как мне кажется, вызывают у вас проблемы. Когда я впервые взглянул на этот вопрос, я предположил, что код, вероятно, был сгенерирован компилятором, а jmp rax
, вероятно, был результатом некоторого оператора потока управления. Наиболее вероятный способ создания такой кодовой последовательности - через C switch
. Нередко оператор switch
из таблицы переходов сообщает, какой код должен выполняться в зависимости от управляющей переменной. Как пример: управляющая переменная для switch(a)
равна a
.
Все это имело смысл для меня, и я написал ряд комментариев (теперь удаленных), которые в конечном итоге привели к странным адресам памяти, на которые jmp rax
попадет. У меня были поручения, чтобы бежать, но когда я вернулся, у меня был момент ага, что у вас, возможно, было то же самое замешательство, которое я сделал. Эти выходные данные objdump
с использованием опции -s
выглядят как:
.rodata
08b0 01000200 ecfdffff d4fdffff bcfdffff ................
08c0 9cfdffff 7cfdffff 6cfdffff 4cfdffff ....|...l...L...
08d0 3cfdffff 2cfdffff 0cfdffff ecfcffff <...,...........
08e0 d4fcffff b4fcffff 0cfeffff ............
Один из ваших вопросов, похоже, о том, какие значения загружаются здесь. Я никогда не использовал опцию -s
для просмотра данных в разделах и не знал, что, хотя дамп разбивает данные на группы по 4 байта (32-битные значения), они отображаются в порядке байтов, как это отображается в памяти. Сначала я предположил, что выходные данные отображают эти значения от старшего значащего байта к младшему значащему байту, и objdump -s
сделал преобразование. Это не тот случай.
Вы должны вручную обратить байты каждой группы из 4 байтов, чтобы получить реальное значение, которое будет считано из памяти, в регистр.
ecfdffff
на выходе фактически означает ec fd ff ff
. В качестве значения DWORD (32-разрядного) вам нужно обратить байты в обратном направлении, чтобы получить значение HEX, как и следовало ожидать при загрузке из памяти. ec fd ff ff
обратным будет ff ff fd ec
или 32-битное значение 0xfffffdec
. Как только вы поймете это, тогда это станет намного понятнее. Если вы сделаете эту же настройку для всех данных в этой таблице, вы получите:
.rodata
08b0: 0x00020001 0xfffffdec 0xfffffdd4 0xfffffdbc
08c0: 0xfffffd9c 0xfffffd7c 0xfffffd6c 0xfffffd4c
08d0: 0xfffffd3c 0xfffffd2c 0xfffffd0c 0xfffffcec
08e0: 0xfffffcd4 0xfffffcb4 0xfffffe0c
Теперь, если мы посмотрим на ваш код, он начинается с:
530: lea rdx,[rip+0x37d] # 8b4 <_IO_stdin_used+0x4>
Это не загружает данные из памяти, оно вычисляет эффективный адрес некоторых данных и помещает адрес в RDX . Разборка из OBJDUMP отображает код и данные с видом, что они загружены в память, начиная с 0x000000000000. Когда он загружен в память, он может быть размещен по другому адресу. GCC в этом случае создает код, независимый от позиции (PIC). Он генерируется таким образом, что первый байт программы может начинаться с произвольного адреса в памяти.
Комментарий # 8b4
- это та часть, которая нас интересует (после этого вы можете игнорировать информацию). Разборка говорит, что если программа была загружена в 0x0000000000000000, то значение, загруженное в RDX , будет 0x8b4. Как это было достигнуто? Эта инструкция начинается с 0x530, но с относительной адресацией RIP RIP (указатель инструкции) относительно адреса сразу после текущей инструкции. Адрес, использованный дизассемблером, был 0x537 (байт после текущей инструкции - это адрес первого байта следующей инструкции). Инструкция добавляет 0x37d к RIP и получает 0x537 + 0x37d = 0x8b4. Адрес 0x8b4 находится в разделе .rodata
, для которого вам дамп (как обсуждалось выше).
Теперь мы знаем, что RDX содержит базу некоторых данных. jmp rax
предполагает, что это, вероятно, будет таблица из 32-битных значений, которые используются для определения, к какой ячейке памяти переходить, в зависимости от значения в управляющей переменной оператора switch
.
Этот оператор сохраняет значение 0 в виде 32-разрядного значения в стеке.
537: mov DWORD PTR [rsp-0xc],0x0
Похоже, что это переменные, которые компилятор решил сохранить в регистрах (а не в памяти).
53f: movabs r10,0xedd5a792ef95fa9e
549: mov r9d,0xffffffcc
R10 загружается с 64-битным значением 0xedd5a792ef95fa9e. R9D - это младшие 32 бита 64-битного регистра R9 . Значение 0xffffffcc загружается в младшие 32-биты R9 , но есть что-то еще происходит. В 64-битном режиме, если назначением инструкции является 32-битный регистр, CPU автоматически обнуляет расширение значения в верхние 32-битные регистры . Процессор гарантирует нам, что старшие 32 бита обнуляются.
Это NOP
и ничего не делает, кроме как выровнять следующую инструкцию по адресу памяти 0x550. 0x550 - это значение, выровненное по 16 байтов. Это имеет некоторое значение и может указывать на то, что инструкция в 0x550 может быть первой инструкцией в верхней части цикла. Оптимизатор может поместить NOP
s в код для выравнивания первой инструкции в верхней части цикла по 16-байтовому выровненному адресу в памяти по соображениям производительности:
54f: nop
Ранее 32-битная переменная на основе стека в rsp-0xc
была установлена в ноль. Это считывает значение 0 из памяти как 32-битное значение и сохраняет его в EAX . Поскольку EAX является 32-битным регистром, используемым в качестве места назначения для инструкции, CPU автоматически заполняет верхние 32-битовые значения RAX до 0. Таким образом, все из RAX ноль.
550: mov eax,DWORD PTR [rsp-0xc]
EAX теперь сравнивается с 0xd. Если оно выше (ja
), оно переходит к инструкции в 0x57c.
554: cmp eax,0xd
557: ja 57c <main+0x4c>
Затем у нас есть эта инструкция:
559: movsxd rax,DWORD PTR [rdx+rax*4]
movsxd
- это инструкция, которая будет принимать 32-битный исходный операнд (в данном случае 32-битное значение по адресу памяти RDX+RAX*4
), загружать его в нижние 32-битные RAX и затем знак расширяют значение до старших 32 битов RAX . Фактически, если 32-битное значение является отрицательным (старший значащий бит равен 1), старшие 32-биты RAX будут установлены в 1. Если 32-битное значение не является отрицательным, верхние 32-битные RAX будет установлено на 0.
Когда этот код встречается впервые RDX содержит базу некоторой таблицы в 0x8b4 от начала программы, загруженной в память. RAX устанавливается в 0. Фактически первые 32 бита в таблице копируются в RAX и расширяются в знаке. Как видно ранее, значение по смещению 0xb84 равно 0xfffffdec. Это 32-битное значение является отрицательным, поэтому RAX содержит 0xfffffffffffffdec.
Теперь к сути ситуации:
55d: add rax,rdx
560: jmp rax
RDX по-прежнему содержит адрес начала таблицы в памяти. RAX добавляется к этому значению и сохраняется в RAX ( RAX = RAX + RDX ). Затем мы JMP по адресу, хранящемуся в RAX . Таким образом, весь этот код предполагает, что у нас есть таблица JUMP с 32-битными значениями, которую мы используем, чтобы определить, куда нам следует идти. Итак, очевидный вопрос. Каковы 32-битные значения в таблице? 32-битные значения - это разница между началом таблицы и адресом инструкции, к которой мы хотим перейти.
Мы знаем, что таблица 0x8b4 из того места, где наша программа загружена в память. Компилятор C сказал компоновщику вычислить разницу между 0x8b4 и адресом, где находится инструкция, которую мы хотим выполнить. Если бы программа была загружена в память по адресу 0x0000000000000000 (гипотетически), RAX = RAX + RDX привел бы к RAX , равному 0xfffffffffffffdec + 0x8b4 = 0x00000000000006a0. Затем мы используем jmp rax
, чтобы перейти к 0x6a0. Вы не показали весь дамп памяти, но будет код 0x6a0, который будет выполняться, когда значение, переданное оператору switch
, равно 0. Каждое 32-разрядное значение в таблице JUMP будет иметь смещение, аналогичное код, который будет выполняться в зависимости от управляющей переменной в операторе switch
. Если мы добавим 0x8b4 ко всем записям в таблице, мы получим:
08b0: 0x000006a0 0x00000688 0x00000670
08c0: 0x00000650 0x00000630 0x00000620 0x00000600
08d0: 0x000005F0 0x000005e0 0x000005c0 0x000005a0
08e0: 0x00000588 0x00000568 0x000006c0
Вы должны обнаружить, что в коде, который вы не предоставили нам, эти адреса совпадают с кодом, который появляется после jmp rax
.
Учитывая, что адрес памяти 0x550 был выровнен, у меня есть предположение, чтоЭтот оператор switch
находится внутри цикла, который продолжает выполняться как конечный автомат , пока не будут выполнены надлежащие условия для его выхода.Вероятно, значение управляющей переменной, используемой для оператора switch
, изменяется кодом в самом операторе switch
.Каждый раз, когда выполняется оператор switch
, управляющая переменная имеет другое значение и будет делать что-то другое.
Управляющая переменная для оператора switch
первоначально проверялась на значение выше 0x0d (13).Таблица, начинающаяся с 0x8b4 в разделе .rodata
, содержит 14 записей.Можно предположить, что оператор switch
, вероятно, имеет 14 различных состояний (случаев).