Как процессор узнает, сколько байтов он должен прочитать для следующей инструкции, учитывая, что инструкции имеют различную длину? - PullRequest
2 голосов
/ 31 мая 2019

Итак, я читал статью, и в ней они сказали, что статическая дизассемблирование двоичного кода неразрешимо, потому что последовательность байтов может быть представлена ​​как можно многими способами, как показано на рисунке (его x86)

disassembling

, поэтому мой вопрос:

  1. как тогда CPU выполняет это?например, на рисунке, когда мы достигаем после C3, как он узнает, сколько байтов он должен прочитать для следующей инструкции?

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

  3. Если ЦП может каким-то образом узнать, сколько байтов он должен прочитать для следующей инструкции, или, в основном, как интерпретировать следующую инструкцию, почему мы не можем сделать это статически?

Ответы [ 3 ]

3 голосов
/ 31 мая 2019

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

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

Инструкция RET (retn) в вашем примере может быть концом функции. Функции часто заканчиваются инструкцией e RET, но не обязательно так. Для функции возможно иметь несколько инструкций RET, ни одна из которых не находится в конце функции. Вместо этого последняя инструкция будет своего рода JMP-инструкцией, которая возвращается к некоторому месту в функции или к другой функции целиком.

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


Набор команд x86, в частности, имеет довольно сложный формат необязательных байтов префикса, одного или нескольких байтов кода операции, одного или двух возможных байтов формы адресации, а затем возможных байтов смещения и непосредственных байтов. Байты префикса могут быть добавлены к любой инструкции. Байты кода операции определяют, сколько существует байтов кода операции и может ли инструкция иметь байты операнда и непосредственные байты. Код операции также может указывать на наличие байтов смещения. Первый байт операнда определяет, есть ли второй байт операнда и есть ли байты смещения.

Руководство разработчика программного обеспечения для архитектуры Intel 64 и IA-32 имеет следующий рисунок, показывающий формат инструкций x86:

X86 Instruction Format

Python-подобный псевдокод для декодирования инструкций x86 будет выглядеть примерно так:

# read possible prefixes

prefixes = []
while is_prefix(memory[IP]):
    prefixes.append(memory[IP))
    IP += 1

# read the opcode 

opcode = [memory[IP]]
IP += 1
while not is_opcode_complete(opcode):
    opcode.append(memory[IP])
    IP += 1

# read addressing form bytes, if any

modrm = None
addressing_form = []    
if opcode_has_modrm_byte(opcode):
    modrm = memory[IP]
    IP += 1
    if modrm_has_sib_byte(modrm):
        addressing_form = [modrm, memory[IP]]
        IP += 1
    else:
        addressing_form = [modrm]

# read displacement bytes, if any

displacement = []
if (opcode_has_displacement_bytes(opcode)
    or modrm_has_displacement_bytes(modrm)):
    length = determine_displacement_length(prefixes, opcode, modrm)
    displacement = memory[IP : IP + length]
    IP += length

# read immediate bytes, if any

immediate = []
if opcode_has_immediate_bytes(opcode):
    length = determine_immediate_length(prefixes, opcode)
    immediate = memory[IP : IP + length]
    IP += length

# the full instruction

instruction = prefixes + opcode + addressing_form + displacement + immediate

Одна важная деталь, оставленная в псевдокоде выше, состоит в том, что длина инструкций ограничена 15 байтами. Можно построить иные действительные инструкции x86 длиной 16 байтов или более, но такая инструкция при выполнении вызовет неопределенное исключение ЦП кода операции. (Есть и другие детали, которые я пропустил, например, как часть кода операции может быть закодирована внутри байта Mod R / M, но я не думаю, что это влияет на длину инструкций.)


Однако процессоры x86 на самом деле не декодируют инструкции, как я описал выше, они только декодируют инструкции, как будто они читают каждый байт по одному за раз. Вместо этого современные процессоры будут считывать целые 15 байтов в буфер, а затем декодировать байты параллельно, обычно за один цикл. Когда он полностью декодирует инструкцию, определяя ее длину, и готов прочитать следующую инструкцию, он сдвигает оставшиеся байты в буфере, которые не были частью инструкции. Затем он считывает больше байтов, чтобы снова заполнить буфер до 15 байтов, и начинает декодирование следующей инструкции.

Еще одна вещь, которую будут делать современные процессоры, это не подразумевается тем, что я написал выше, это умозрительно выполнять инструкции.Это означает, что ЦП будет декодировать инструкции и предварительно попытаться выполнить их даже до того, как завершит выполнение предыдущих инструкций.Это, в свою очередь, означает, что ЦП может в конечном итоге декодировать инструкции, следующие за инструкцией RET, но только если он не может определить, куда будет возвращаться RET.Поскольку из-за попыток декодирования и условного выполнения случайных данных, которые не предназначены для выполнения, могут быть потери производительности, компиляторы обычно не помещают данные между функциями.Хотя они могут заполнить это пространство инструкциями NOP, которые никогда не будут выполняться для выравнивания функций по соображениям производительности.

(Раньше они размещали данные только для чтения между функциями, но это было до того, как процессоры x86, которые умело могли выполнять инструкции, стали обычным явлением.)

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

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

Когда код операции читается во время фазы выборки инструкции, сам код операции кодирует один вид инструкции, и секвенсор уже знает, сколько байтов нужно прочитать из нее. Здесь нет двусмысленности. В вашем примере, после извлечения C3, но перед его выполнением, CPU отрегулирует свой регистр EIP (указатель Intruction), чтобы прочитать то, что он считает следующей инструкцией (начинающейся с 0F), НО во время выполнения инструкции C3 ( который является инструкцией RETN), EIP изменяется, поскольку RETN равен «Возврат из подпрограммы), поэтому он не достигнет инструкции 0F 88 52. Эта команда будет достигнута только в том случае, если какая-то другая часть кода перейдет в местоположение этой инструкции Если ни один код не выполняет такой переход, то он будет считаться данными, но проблема определения того, будет или не будет выполнена конкретная инструкция, не решаема.

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

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

1 голос
/ 31 мая 2019

Ваша главная проблема, кажется, следующая:

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

Проблема, описанная в статье, связана с «прыгающими» инструкциями (что означает не только jmp, но также int, ret, syscall и аналогичные инструкции):

Цель таких инструкций состоит в том, чтобы продолжить выполнение программы по совершенно другому адресу вместо продолжения при следующей инструкции. (Вызовы функций и циклы while() являются примерами, когда выполнение программы не продолжается при следующей инструкции.)

Ваш пример начинается с инструкции jmp eax, которая означает, что значение в регистре eax решает, какая инструкция будет выполнена после инструкции jmp eax.

Если eax содержит адрес байта 0F, ЦП выполнит инструкцию jcc (левый регистр на рисунке); если он содержит адрес 88, он выполнит инструкцию mov (средний регистр на рисунке); и если он содержит адрес 52, он выполнит инструкцию push (правый регистр на рисунке).

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

(Мне сказали, что в 1980-х годах даже были коммерческие программы, в которых во время выполнения возникали разные случаи: в вашем примере это означало бы, что иногда выполняется команда jcc, а иногда - mov!)

Когда мы достигаем после C3, как он узнает, сколько байтов следует прочитать для следующей инструкции?

Как процессор знает, на сколько он должен увеличивать ПК после выполнения одной инструкции?

C3 не является хорошим примером, потому что retn является инструкцией «прыжка»: «инструкция после C3» никогда не будет достигнута, потому что выполнение программы продолжается в другом месте.

Однако вы можете заменить C3 другой инструкцией длиной в один байт (например, 52). В этом случае четко определено, что следующая инструкция будет начинаться с байта 0F, а не с 88 или 52.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...