Нахождение количества операндов в инструкции по кодам операций

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

69 62 2f 6c 64 2d 6c

что должно соответствовать:

imul   $0x6c2d646c,0x2f(%edx),%esp

Теперь инструкция imul может иметь два или три операнда. Как мне понять это из кодов операций, которые у меня есть?

Он основан на наборе инструкций Intel i386.


person Hrishikesh Murali    schedule 03.08.2011    source источник
comment
Дизассемблер для какого набора инструкций?   -  person Karl Knechtel    schedule 03.08.2011
comment
Посмотрите это в руководствах Intel?   -  person jalf    schedule 03.08.2011
comment
О, простите, я забыл упомянуть. Набор инструкций Intel i386. Добавлю сразу.   -  person Hrishikesh Murali    schedule 03.08.2011
comment
Посмотрел на intel.com/Assets/PDF/manual/325383.pdf и AFAIK не описывает, как кодируется операция imul с 3 операндами.   -  person Hrishikesh Murali    schedule 03.08.2011
comment
Вы уверены, что это опкоды? Их интерпретация как коды ASCII — ib/ld-l — предполагает, что они могут быть чем-то другим.   -  person Marcelo Cantos    schedule 03.08.2011


Ответы (5)


Хотя набор инструкций x86 довольно сложен (в любом случае это CISC) и я видел здесь много людей, которые отговаривают ваши попытки понять его, я скажу наоборот: его все же можно понять, и вы можете узнать по пути о почему он такой сложный и как Intel удалось несколько раз расширить его от 8086 до современных процессоров.

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

Например, каждому коду операции может предшествовать от нуля до четырех байтов префикса, которые являются необязательными. Обычно вам не нужно беспокоиться о них. Они используются для изменения размера операндов или как escape-коды на «второй этаж» таблицы кодов операций с расширенными инструкциями современных ЦП (MMX, SSE и т. д.).

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

Далее идет так называемый байт ModR/M (иногда также называемый mode-reg-reg/mem), который кодирует режим адресации и типы операндов. Он используется только теми кодами операций, у которых есть подобные операнды. Он имеет три битовых поля:

  • Первые два бита (слева, самые значащие) кодируют режим адресации (4 возможных комбинации битов).
  • Следующие три бита кодируют первый регистр (8 возможных битовых комбинаций).
  • Последние три бита могут кодировать другой регистр или расширять режим адресации, в зависимости от того, как настроены первые два бита.

После байта ModR/M может быть еще один необязательный байт (в зависимости от режима адресации) с именем SIB (Scale Index Base). Он используется для более экзотических режимов адресации для кодирования коэффициента масштабирования (1x, 2x, 4x), базового адреса/регистра и индексного регистра. Он имеет такое же расположение, как и байт ModR/M, но первые два бита слева (самые значащие) используются для кодирования шкалы, а следующие три и последние три бита кодируют индекс и базовые регистры, как следует из названия.

Если используется какое-либо смещение, оно идет сразу после этого. Он может иметь длину 0, 1, 2 или 4 байта, в зависимости от режима адресации и режима выполнения (16-бит/32-бит/64-бит).

Последний всегда является непосредственными данными, если таковые имеются. Он также может иметь длину 0, 1, 2 или 4 байта.

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

Например, все кодировки регистров следуют аккуратному шаблону ACDB. То есть для 8-битных инструкций младшие два бита кода регистра кодируют регистры A, C, D и B соответственно:

00 = A регистр (накопитель)
01 = C регистр (счетчик)
10 = D регистр (данные)
11 = B регистр (база)

Я подозреваю, что их 8-битные процессоры использовали как раз эти четыре 8-битных регистра, закодированных таким образом:

       second
      +---+---+
f     | 0 | 1 |          00 = A
i +---+---+---+          01 = C
r | 0 | A : C |          10 = D
s +---+ - + - +          11 = B
t | 1 | D : B |
  +---+---+---+

Затем на 16-битных процессорах они удвоили этот банк регистров и добавили еще один бит в кодировке регистра для выбора банка, вот так:

       second                second         0 00  =  AL
      +----+----+           +----+----+     0 01  =  CL
f     | 0  | 1  |     f     | 0  | 1  |     0 10  =  DL
i +---+----+----+     i +---+----+----+     0 11  =  BL
r | 0 | AL : CL |     r | 0 | AH : CH |
s +---+ - -+ - -+     s +---+ - -+ - -+     1 00  =  AH
t | 1 | DL : BL |     t | 1 | DH : BH |     1 01  =  CH
  +---+---+-----+       +---+----+----+     1 10  =  DH
    0 = BANK L              1 = BANK H      1 11  =  BH

Но теперь вы также можете использовать обе половины этих регистров вместе, как полные 16-битные регистры. Это делается с помощью последнего бита кода операции (самый младший бит, самый правый): если это 0, это 8-битная инструкция. Но если этот бит установлен (то есть опкод — нечетное число), это 16-битная инструкция. В этом режиме два бита, как и раньше, кодируют один из регистров ACDB. Узоры остаются прежними. Но теперь они кодируют полные 16-битные регистры. Но когда также установлен третий байт (самый старший), они переключаются на совершенно другой банк регистров, называемый регистрами индекса/указателя, а именно: SP (указатель стека), BP (базовый указатель), SI (исходный индекс) , DI (индекс назначения/данных). Таким образом, адресация теперь выглядит следующим образом:

       second                second         0 00  =  AX
      +----+----+           +----+----+     0 01  =  CX
f     | 0  | 1  |     f     | 0  | 1  |     0 10  =  DX
i +---+----+----+     i +---+----+----+     0 11  =  BX
r | 0 | AX : CX |     r | 0 | SP : BP |
s +---+ - -+ - -+     s +---+ - -+ - -+     1 00  =  SP
t | 1 | DX : BX |     t | 1 | SI : DI |     1 01  =  BP
  +---+----+----+       +---+----+----+     1 10  =  SI
    0 = BANK OF           1 = BANK OF       1 11  =  DI
  GENERAL-PURPOSE        POINTER/INDEX
     REGISTERS             REGISTERS

При внедрении 32-битных процессоров эти банки снова удвоились. Но схема остается прежней. Только теперь нечетные опкоды означают 32-битные регистры, а четные опкоды, как и прежде, 8-битные регистры. Я бы назвал нечетные коды операций «длинными» версиями, потому что 16/32-битная версия используется в зависимости от процессора и его текущего режима работы. Когда он работает в 16-битном режиме, нечетные («длинные») коды операций означают 16-битные регистры, но когда он работает в 32-битном режиме, нечетные («длинные») коды операций означают 32-битные регистры. Его можно перевернуть, поставив перед всей инструкцией префикс 66 (переопределение размера операнда). Четные коды операций («короткие») всегда 8-битные. Таким образом, в 32-битном процессоре коды регистров следующие:

0 00 = EAX      1 00 = ESP
0 01 = ECX      1 01 = EBP
0 10 = EDX      1 10 = ESI
0 11 = EBX      1 11 = EDI

Как видите, шаблон ACDB остался прежним. Также шаблон SP,BP,SI,SI остается прежним. Он просто использует более длинные версии регистров.

В кодах операций также есть некоторые закономерности. Один из них я уже описал (четные и нечетные = 8-битные «короткие» и 16/32-битные «длинные»). Больше из них вы можете увидеть на этой карте кодов операций, которую я сделал однажды для быстрой ссылки и ручной сборки/разборки:   введите описание изображения здесь (Это еще не полная таблица, некоторые коды операций отсутствуют. Может быть, я когда-нибудь ее обновлю.)

Как видите, арифметические и логические инструкции в основном расположены в верхней половине таблицы, а левая и правая ее половины имеют аналогичную компоновку. Инструкции по перемещению данных находятся в нижней половине. Все инструкции ветвления (условные переходы) находятся в строке 7*. Также есть одна полная строка B*, зарезервированная для инструкции mov, которая является сокращением для загрузки непосредственных значений (констант) в регистры. Все они представляют собой однобайтовые коды операций, за которыми сразу следует непосредственная константа, потому что они кодируют регистр назначения в коде операции (они выбираются по номеру столбца в этой таблице) в его трех младших байтах (крайние правые) . Они следуют одному и тому же шаблону для кодирования регистров. И четвертый бит - это "короткий"/"длинный" выбор. Вы можете видеть, что ваша инструкция imul уже находится в таблице точно в позиции 69 (хм... ;J).

Во многих инструкциях бит непосредственно перед «коротким/длинным» битом предназначен для кодирования порядка операндов: какой из двух регистров, закодированных в ModR/M байте, является источником, а какой — приемником (это относится к инструкции с двумя регистровыми операндами).

Что касается поля режима адресации байта ModR/M, вот как его интерпретировать:

  • 11 является самым простым: он кодирует передачи между регистрами. Один регистр кодируется тремя следующими битами (поле reg), а другой регистр - остальными тремя битами (поле R/M) этого байта.
  • 01 означает, что после этого байта будет однобайтовое смещение.
  • 10 означает то же самое, но используемое смещение составляет четыре байта (на 32-разрядных процессорах).
  • 00 самый хитрый: он означает косвенную адресацию или простое смещение, в зависимости от содержимого поля R/M.

Если байт SIB присутствует, об этом сигнализирует битовая комбинация 100 в R/M битах. Также есть код 101 для 32-битного режима только смещения, который вообще не использует байт SIB.

Вот краткое изложение всех этих режимов адресации:

Mod R/M
 11 rrr = register-register  (one encoded in `R/M` bits, the other one in `reg` bits).
 00 rrr = [ register ]       (except SP and BP, which are encoded in `SIB` byte)
 00 100 = SIB byte present
 00 101 = 32-bit displacement only (no `SIB` byte required)
 01 rrr = [ rrr + disp8 ]    (8-bit displacement after the `ModR/M` byte)
 01 100 = SIB + disp8
 10 rrr = [ rrr + disp32 ]   (except SP, which means that the `SIB` byte is used)
 10 100 = SIB + disp32

Итак, давайте теперь расшифруем ваш imul:

69 — это код операции. Он кодирует версию imul, которая не расширяет по знаку 8-битные операнды. Версия 6B действительно расширяет их. (Они отличаются битом 1 в коде операции, если кто-то спрашивал.)

62 - это RegR/M байт. В двоичном формате это 0110 0010 или 01 100 010. Первые два байта (поле Mod) означают режим косвенной адресации и то, что смещение будет 8-битным. Следующие три бита (поле reg) равны 100 и кодируют регистр SP (в данном случае ESP, поскольку мы находимся в 32-битном режиме) как регистр назначения. Последние три бита — это поле R/M, и у нас есть 010, которые кодируют регистр D (в данном случае EDX) как другой (исходный) используемый регистр.

Теперь мы ожидаем 8-битное смещение. И вот оно: 2f — смещение, положительное (+47 в десятичном выражении).

Последняя часть — это четыре байта непосредственной константы, которая требуется для инструкции imul. В вашем случае это 6c 64 2d 6c, что в прямом порядке равно $6c2d646c.

Вот так и рассыпается печенье ;-J

person Community    schedule 19.08.2013

В руководствах описывается, как различать версии с одним, двумя или тремя операндами.

Инструкция IMUL

F6/F7: один операнд; 0F AF: два операнда; 6B/69: три операнда.

person Igor Skochinsky    schedule 03.08.2011
comment
Спасибо, я совсем проглядел эту таблицу. :-( - person Hrishikesh Murali; 03.08.2011
comment
как вы декодируете 16-битный непосредственный регистр из 32-битного непосредственного кода операции 0x69? - person old_timer; 03.08.2011
comment
@dwlech: в зависимости от текущего размера операнда по умолчанию (16- или 32-разрядный) код операции имеет префикс 0x66 (префикс переопределения размера операнда). Например. в 32-битном режиме 69 .. означает IMUL r32, r/m32, а 66 69 .. означает IMUL r16, r/m16. - person user786653; 03.08.2011

Небольшой совет: сначала соберите всю документацию по набору инструкций, до которой сможете дотянуться. для этого случая x86 попробуйте некоторые старые руководства по 8088/86, а также более свежие, от Intel, а также множество таблиц кодов операций в сети. различная интерпретация и документация могут, во-первых, иметь незначительные ошибки или различия в документации, а во-вторых, некоторые люди могут представлять информацию другим и более понятным способом.

Во-вторых, если это ваш первый дизассемблер, я рекомендую избегать x86, это очень сложно. Поскольку ваш вопрос подразумевает, что наборы инструкций переменной длины слова сложны, чтобы сделать удаленно успешный дизассемблер, вам нужно следовать коду в порядке выполнения, а не в порядке памяти. Таким образом, ваш дизассемблер должен использовать какую-то схему, чтобы не только декодировать и печатать инструкции, но и декодировать инструкции перехода и помечать адреса назначения как точки входа в инструкцию. например, ARM имеет фиксированную длину инструкции, вы можете написать дизассемблер ARM, который запускается в начале оперативной памяти и дизассемблирует каждое слово напрямую (при условии, конечно, что это не смесь кода руки и большого пальца). thumb (не thumb2) может быть дизассемблирован таким образом, поскольку существует только один вариант 32-битной инструкции, все остальные — 16-битные, и этот вариант может быть обработан в простой машине состояний, поскольку эти две 16-битные инструкции отображаются как пары.

Вы не сможете дизассемблировать все (с набором инструкций переменной длины), и из-за нюансов некоторого ручного кодирования или преднамеренной тактики предотвращения дизассемблирования ваш предварительный код, который проходит код в порядке выполнения, может иметь то, что я бы назвал столкновение, например ваши инструкции выше. Скажем, один путь ведет вас к 0x69, являющемуся точкой входа в инструкцию, и вы определяете, что это 7-байтовая инструкция, но скажем, где-то еще есть инструкция ветвления, пункт назначения которой вычисляется как 0x2f, являющийся кодом операции для инструкции, хотя очень умное программирование может сделать что-то подобное, более вероятно, что дизассемблер был направлен на дизассемблирование данных. Например

clear condition flag
branch if condition flag clear
data

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

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

Короткий ответ: Intel публиковала и, возможно, до сих пор публикует технические справочные руководства для процессоров, у меня до сих пор есть руководства по 8088/86, аппаратные для электрических компонентов и программные для набора инструкций и того, как это работает. У меня есть 486 и, возможно, 386. Снимок в ответе Игоря прямо напоминает руководство Intel. Поскольку набор инструкций так сильно изменился с течением времени, x86 в лучшем случае является трудным зверем. В то же время, если сам процессор может продираться через эти байты и выполнять их, вы можете написать программу, которая может делать то же самое, но декодировать их. разница в том, что вы, скорее всего, не собираетесь создавать симулятор, и любые ветки, которые вычисляются кодом и не являются явными в коде, вы не сможете увидеть, и пункт назначения для этой ветки может не отображаться в вашем списке байтов для разобрать.

person old_timer    schedule 03.08.2011
comment
Огромное спасибо, постараюсь разобраться как можно больше и постараюсь допилить свой дизассемблер. - person Hrishikesh Murali; 15.11.2011

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

Это часть текстовой строки, она переводится как:

$ echo -e "\x69\x62\x2f\x6c\x64\x2d\x6c"
ib/ld-l

который, очевидно, является частью строки "/lib/ld-linux.so.2".

person ninjalj    schedule 04.08.2011
comment
Возможно ты прав. Но это еще вопрос интерпретации. Байты - это просто байты. Вам (или процессору) решать, правильно ли их интерпретировать. Я предполагаю, что ОП просто пытался разобрать какие-то случайные данные, чтобы посмотреть, что покажет его дизассемблер, а затем выяснить, почему это так. Это хороший способ узнать, как работают дизассемблеры и как кодируются инструкции. Так что попробовать все-таки стоит. - person SasQ; 19.08.2013
comment
Однажды я попытался реконструировать формат кодирования инструкций, поэтому я просто написал простой скрипт для заполнения файла последующими числами, чтобы посмотреть, как они будут интерпретированы моим дизассемблером. Разумным способом сделать это являются последующие числа, потому что тогда вы сможете увидеть, как простые изменения в последовательности битов отразятся на дизассемблированной инструкции, и есть хорошие шансы, что изменится только небольшая часть инструкции. Например. попробуйте с 88 C0 на 88 CF, а затем с 89 C0 на 89 CF, и вы поймете шаблон адресации регистров. - person SasQ; 19.08.2013

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

person Necrolis    schedule 03.08.2011
comment
Я делаю это, чтобы прочно усвоить основы ассемблера, программируя дизассемблер. :-) - person Hrishikesh Murali; 03.08.2011
comment
@Hrishikesh Murali: я бы сказал, что вам лучше программировать что-то на ассемблере, IMO, процедура дизассемблера больше подходит для тех, кто хочет писать генераторы кода / JIT. но удачи в любом случае :) - person Necrolis; 03.08.2011