Как понять поток этого кода сборки - PullRequest
1 голос
/ 24 мая 2019

Я не могу понять, как это работает.
Вот часть программы main (), разобранная objdump и написанная в нотации Intel

0000000000000530 <main>:
530:    lea    rdx,[rip+0x37d]        # 8b4 <_IO_stdin_used+0x4>
537:    mov    DWORD PTR [rsp-0xc],0x0
53f:    movabs r10,0xedd5a792ef95fa9e 
549:    mov    r9d,0xffffffcc
54f:    nop
550:    mov    eax,DWORD PTR [rsp-0xc]
554:    cmp    eax,0xd
557:    ja     57c <main+0x4c>
559:   movsxd rax,DWORD PTR [rdx+rax*4]
55d:    add    rax,rdx
560:    jmp    rax

Дамп раздела rodata:

.rodata 
 08b0 01000200 ecfdffff d4fdffff bcfdffff  ................
 08c0 9cfdffff 7cfdffff 6cfdffff 4cfdffff  ....|...l...L...
 08d0 3cfdffff 2cfdffff 0cfdffff ecfcffff  <...,...........
 08e0 d4fcffff b4fcffff 0cfeffff           ............

В 530 rip равен [537], поэтому [rdx] = [537 + 37d] = 8b4.
Первый вопрос: насколько велика ценность rdx? Значение - это ec, или ecfdffff, или что-то еще? Если у него есть DWORD, я могу понять, что у него есть 'ecfdffff' (даже это тоже неправильно? :(), но эта программа не объявляет его. Как я могу оценить значение?

Затем программа продолжается.
В 559 году впервые появляется ракс.
Второй вопрос: этот rax может интерпретироваться как часть eax и в это время rax = 0? Если rax равно 0, в 559 означает rax = DWORD [rdx], а значение rax становится ecfdffff, а в следующем [55d] используется rax + = rdx, и я думаю, что это значение не может сработать. Там должно быть что-то не так, поэтому скажите мне, где или как я делаю какие-либо ошибки.

Ответы [ 2 ]

2 голосов
/ 25 мая 2019

Я думаю, что я отклонюсь от того, что Питер обсуждал (он предоставляет хорошую информацию), и расскажу о некоторых проблемах, которые, как мне кажется, вызывают у вас проблемы. Когда я впервые взглянул на этот вопрос, я предположил, что код, вероятно, был сгенерирован компилятором, а 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 различных состояний (случаев).

2 голосов
/ 24 мая 2019

но эта программа не объявляет об этом

Вы смотрите на разборку машинного кода + данные. Это всего лишь байты в памяти. Любые метки, которые дизассемблеру удается показать, это те, которые остались в таблице символов исполняемого файла. Они не имеют отношения к тому, как процессор выполняет машинный код.

(Заголовки программ ELF сообщают загрузчику программ ОС, как отобразить его в память и куда перейти в качестве точки входа. Это не имеет ничего общего с символами, если общая библиотека не ссылается на некоторые глобальные переменные или функции, определенные в исполняемый файл.)

Вы можете пошагово кодировать в GDB и наблюдать изменение значений регистра.


В 559 году rax впервые появился.

EAX - это младшие 32 бита RAX. Запись в EAX неявно распространяется на RAX. Из mov DWORD PTR [rsp-0xc],0x0 и более поздней перезагрузки мы знаем, что RAX = 0.

Это должен был быть неоптимизированный вывод компилятора (или volatile int idx = 0; для предотвращения постоянного распространения) , иначе во время компиляции он знал бы, что RAX = 0, и мог бы оптимизировать все остальное.


lea rdx,[rip+0x37d] # 8b4

REA-относительный LEA помещает адрес статического в регистр. Это не загрузка из памяти. (Это происходит позже, когда movsxd в режиме индексированной адресации использует RDX в качестве базового адреса.)

дизассемблер разработал адрес для вас; это RDX = 0x8b4. (Относительно начала файла; при фактическом запуске программа будет отображаться по виртуальному адресу, например 0x55555...000)


554:    cmp    eax,0xd
557:    ja     57c <main+0x4c>
559:   movsxd rax,DWORD PTR [rdx+rax*4]
55d:    add    rax,rdx
560:    jmp    rax

Это таблица прыжков. Сначала он проверяет наличие внепланового индекса с помощью cmp eax,0xd, затем индексирует таблицу 32-битных смещений со знаком с помощью EAX (movsxd с режимом адресации, который масштабирует RAX на 4), и добавляет его к базе. адрес таблицы для получения цели прыжка.

GCC может просто создать таблицу переходов из 64-битных абсолютных указателей, но решит не делать так, чтобы .rodata также не зависел от позиции и не нуждался в исправлениях времени загрузки в PIE исполняемый файл. (Даже несмотря на то, что Linux поддерживает это.) См. https://gcc.gnu.org/bugzilla/show_bug.cgi?id=84011, где это обсуждается (хотя основное внимание этой ошибки заключается в том, что gcc -fPIE не может превратить коммутатор в таблицу поиска строковых адресов, и на самом деле все еще использует таблицу переходов)

Адрес таблицы смещения перехода находится в RDX , это то, что было установлено с более ранним LEA.

...