Выпуск 7
[Main] [TMC 1] [TMC 2] [About]
[ 1 2 3 4 5 6 7 * ]


Доброе время суток, уважаемые читатели!

Выпуск этой рассылки по времени совпал с праздником 8 марта, поэтому пользуясь случаем, хочу поздравить с этим замечательным праздником прекрасную половину читателей этой рассылки. Хочу пожелать вам здоровья, любви, хорошего настроения, молодости и красоты!


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

Теперь у рассылки появился сайт! И адрес у него вот такой - http://tmc.sf.net/ . Мне его очень давно не хватало, но вот создать его толком было некогда. Он и сейчас нуждается в серьезной доработке, как в плане дизайна и логической структуры, так и в плане наполнения материалом. По крайней мере, теперь есть где разместить выпуски рассылки и сопутствующие им материалы.

Тематику сегодняшнего выпуска вначале планировалась выбрать несколько по-другому, но потом я решил, что лучше вначале логически завершить программную часть. В сегодняшнем выпуске мы поговорим о дизассемблере.

Что же такое дизассемблирование?

Вначале кратко о том, что собой представляет дизассемблирование. Как следует из названия, этот процесс обратный ассемблированию. Поэтому не сильно погрешив против истины, можно считать, что дизассемблирование - исполнение алгоритма, который получает на входе бинарный исполнимый код, а на выходе выдает его мнемоническую запись на языке ассемблера. При этом важно отметить, что далеко не всегда получаемый текст совпадает с исходным ассемблерным. Как правило, дизассемблер совершенно не в состоянии восстановить названия меток или точно команду. С названиями меток все просто, зачастую они отсутствуют в бинарном файле, потому что пересчитываются непосредственно в адреса, и необходимость в них отпадает. Что же касается восстановления команд, то это менее критично. Например, в семействе микропроцессоров x86 компании Intel для машинной инструкции 90h используются два мнемонических обозначения "NOP" и "XCHG AX,AX" (см. Intel Architecture Software Developer's Manual, volume 2: Instruction Set Reference, p. 340)

Впрочем, могут встретится более тонкие и важные моменты. Например, Kris Kaspersky в статье "Тонкости дизассемблирования" указывает на следующие особенности:

Обратим внимание так же и на последовательности типа 0x66 0x66 [xxx].

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

Есть еще один интересный момент связанный с работой декодера микропроцессора.

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

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

Префиксы переопределения сегмента могут встречаться перед любой командой, в том числе и не обращающейся к памяти, например, CS:NOP вполне успешно выполнится. А вот некоторые дизассемблеры сбиться могут. К счастью, IDA Pro к ним не относится. Самое интересное, что комбинация 'DS: FS: FG: CS: MOV AX,[100]' работает вполне нормально (хотя это и не гарантируется фирмой Intel). При этом последний префикс в цепочке перекрывает все остальные. Некоторые отладчики, наоборот, ориентируются на первый префикс в цепочке, что дает неверный результат.

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

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

Экскурс в историю создания

В процессе работы над TMC 1, параллельно с разработкой ассемблера было решено создать дизассемблер для автоматического тестирования первого. Идея вынашивалась долго и была реализована в течение суток. Вообще-то говоря, хронологически дизассемблер относится ко времени TMC 2 и написан на языке Си++. Однако, он разработан под архитектуру TMC 1 и полностью зависим от той спецификации.

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

Процесс этот был настолько увлекательный и стремительный, что у меня толком не осталось черновиков с собственными комментариями и подпрограммами ПЗУ. Восстановить тексты мне помог мой коллега по цеху (и участник KMS) - Flash (e-mail: birya at km.ru, FidoNet: 2:5061/52.777). Эти тексты служат отличной иллюстрацией ассемблера. Для всех, кто знаком с Intel x86 все должно быть предельно понятно. На сайт выложен черновик документации, которая использовалась при разработке ассемблера. Она может пояснить особенности кодирования инструкций. Также на сайте есть таблицы, необходимые для кодирования команд. На любые вопросы тех, кто отважится покопаться в этих спецификациях, я обязательно отвечу. Это позволит прояснить реальную ситуацию с документированием.

Раскопки Flash-а

Первый фрагмент представляет собой начальную загрузку. На него передается управление при старте машины. Смысл пока нас не будет интересовать. Основное - уловить типы команд и выполняемые каждой действия.

   --------------------------------------------------
   Номер фрагмента: 1
   Смещение в файле: 0x0000 (0) - 0x0084 (132).
   Длина фрагмента: 133 байта.
   --------------------------------------------------

   c000    mov             WORD    AR7, c000
   c004    mov             WORD    [1fe], ef00
   c00a    mov             WORD    [40], c100
   c010    mov             BYTE    R00, fe
   c013    in              BYTE    R00, 30
   c016    sti
   c017    nop
   c018    nop
   c019    nop
   c01a    nop
   c01b    nop
   c01c    nop
   c01d    nop
   c01e    nop
   c01f    nop
   c020    mov             BYTE    R01, 0
   c023    in              BYTE    R01, 40
   c026    out             WORD    R0, 41
   c029    inc             WORD    R0
   c02b    in              BYTE    R01, 40
   c02e    in              WORD    R0, 41
   c031    mov             WORD    [200], R0
   c035    nop
   c036    nop
   c037    nop
   c038    nop
   c039    nop
   c03a    nop
   c03b    nop
   c03c    nop
   c03d    nop
   c03e    nop
   c03f    nop
   c040    mov             WORD    AR0, cf00
   c044    mov             WORD    AR1, f000
   c048    mov             WORD    R7, 0
   c04c    int             BYTE    ff
   c04e    mov             WORD    AR0, d000
   c052    mov             WORD    AR1, f190
   c056    int             BYTE    ff
   c058    mov             WORD    AR1, f1a7
   c05c    mov             WORD    R7, 1
   c060    int             BYTE    ff
   c062    mov             WORD    AR0, d020
   c066    mov             WORD    AR1, f1b8
   c06a    mov             WORD    R7, 0
   c06e    int             BYTE    ff
   c070    mov             WORD    AR1, f1c4
   c074    mov             WORD    R7, 1
   c078    mov             WORD    R7, [AR0]
   c07a    add             WORD    [AR1], [AR0]
   c07c    int             BYTE    ff
   c07e    cmp             WORD    R0, 220
   c082    jb/jnae/jc      BYTE    f4
   c084    hlt

На распечатке четыре колонки. Первая - адреса инструкций в памяти. После включения питания, управление передается на адрес 0C000h. Вторая колонка - мнемоническое обозначение инструкции процессора, третий - показывает размер операнда, а четвертый сами операнды.

Инструкции mov, вероятно не нуждаются в пояснении. Их смысл - пересылка данных. Инструкция in совершенно типична для x86. Это чтение из порта ввода-вывода. STI - устанавливает флаг разрешения обработки прерываний. Hlt - останавливает работу процессора. Int - вызов программного прерывания. Add - сложение, cmp - сравнение, jb/jnaе/jc - условные переходы. Этот фрагмент с условным переходом показывает еще одну неоднозначность при дизассемблировании. Дело в том, что все эти команды выполняют одинаковые действия. Для удобства чтения и записи можно использовать Jump if Below (jb) или Jump Not Above or Equal (jnae) или Jump If Carry (jc), т.е. то название, которое больше подходит по смыслу в этот алгоритм. Фактической же разницы между командами нет никакой, ведь можно записать как "a<b", так и "not (a>=b)". Совершенно эквивалентная запись. Собственно, этот фрагмент и есть весь основной код программы демонстрации возможностей. Вся основная работы скрыта в функциях прерывания 255 (0FFh). Они представляют некий аналог программных прерываний ПЗУ. Например, в IBM PC прерывание 10h предоставляет функции для управления выводом, 13h - работу с дисками, 16h - ввод с клавиатуры и т.д.

А вот этот один из немногих черновиков, которые остались еще при разработке. Вначале я написал программу на ассемблере, а затем справа вручную соущствил ее ассемблирование. Подобным образом происходило написание всего ПЗУ. Этот фрагмент - преобразование числа из регистра r0 в строку, заданную регистром ar0:

                                               -----¬
                                               ¦2100¦
                                               L-----


        push    ar0             41 81
        push    ar1             41 83
        push    r0              41 80
        push    r1              41 82

        mov     r1,r0           A1 82
        shr     r0,12           C4 80 0C
        mov     ar0,0EFF0h      A0 81 F0 EF
        add     ar0,r00         01 01
        movb    [ar1],[ar0]     A3 02
        inc     ar1             49 03
        sub     ar0,r00         E1 01
        mov     r00,r11         A1 30
        and     r00,0Fh         10 00 0F

        add     ar0,r00         01 01
        mov     [ar1],[ar0]     A3 02
        inc     ar1             49 03
        sub     ar0,r00         E1 01
        mov     r00,r10         A1 20
        shr     r00,04          C4 00 04

        add     ar0,r00         01 01
        movb    [ar1],[ar0]     A3 02
        inc     ar1             49 03
        sub     ar0,r00         E1 01
        mov     r00,r10         A1 20
        and     r00,0Fh         10 00 0F

        add     ar0,r00         01 01
        movb    [ar1],[ar0]     A3 02
        inc     ar1             49 03
        movb    [ar1],0         A0 42 00

        pop     r1              44 82
        pop     r0              44 80
        pop     ar1             44 83
        pop     ar0             44 81
        iret                    27

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

Программирование дизассемблера

Приводить в рассылке исходный текст дизассемблера не имеет смысла, потому что он довольно большой и технически сложный. Вместо этого вот ссылка [7kb] на него. Кстати, в тексте программы присутствуют основные комментарии. Здесь хотелось бы сказать несколько слов о его сборке и использовании.

Выполнен дизассемблер как консольное приложение и написан с учетом ISO стандарта языка Си++ 98-го года (14882), поэтому должен компилироваться всеми компиляторами, которые поддерживают этот стандарт. Проверялось с GNU C++ 2.95 и MS Visual C++ 6.0 . Весь интерфейс с пользователем ведется через консоль и должен быть понятен, потому что построен на принципе вопрос-ответ. Благодаря такой организации программы удается выполнять скрипты.

Вот пример script.txt и пояснение его формата:

   1: 2                 ; Это пункт меню. Должно всегда быть так.
   2: tmc_1_0.rom       ; Это имя файла с ПЗУ, из которого и будет 
                        ; происходить дизассемблирование.
   3: 0                 ; С какой позиции файла производить 
                        ; дизассемблирование.
   4: 20                ; Размер дизассемблируемого кода в байтах.
   5: 49152             ; Это адрес C000 (в дес. форме) - реальный адрес,
                        ; по которому
                        ; загрузится дизассемблируемый фрагмент. Удобен 
                        ; для документирования.
   6: quit              ; Выход из программы и конец скрипта.

Запускать надо так:

TMC_1_0_Disasm.exe result.txt

Спасибо за внимание.

С уважением, автор рассылки: Виктор Петренко (Top)

[Main] [TMC 1] [TMC 2] [About]
[ 1 2 3 4 5 6 7 * ]