Реализация простого ядра Forth

Будучи по натуре, как говорится, a low-level guy, я не мог пропустить Форт по жизни. Этот язык занимает интересную нишу: с одной стороны это вроде высокоуровнего ассемблера, позволяющего писать практически на ассемблере, а с другой стороны позволяет быстро строить из примитивов весьма высокоуровневые интерактивные системы, даже с интроспекцией, при этом оставаясь на адекватном уровне эффективности.

Знаю, что С - это начало всех начал, и при правильном использовании можно писать очень близко по эффективности к ассемблеру. Но, все же есть еще системы, где компилятору С сложно развернуться. Например, захотел я подыскать компилятор С для Intel 8080, чтобы замутить небанальную программу для Радио-86РК. Из реально собираемого я нашел только пару наследников знаменитого Small-Csmallc-85 и smallc-scc3.

Увы, для простейшей программы типа:

main() {
  static char a;
  for (a = 1; a < 10; ++a) {
     ++a;
  }
}

Генерируется адъ типа:

;main() {
main:
;  static char a;
    dseg
?2: ds  1
    cseg
;  for (a = 1; a < 10; ++a) {
    lxi h,?2
    push    h
    lxi h,1
    pop d
    call    ?pchar
?3:
    lxi h,?2
    call    ?gchar
    push    h
    lxi h,10
    pop d
    call    ?lt
    mov a,h
    ora l
    jnz ?5
    jmp ?6
?4:
    lxi h,?2
    push    h
    call    ?gchar
    inx h
    pop d
    call    ?pchar
    jmp ?3
?5:
;     ++a;
    lxi h,?2
    push    h
    call    ?gchar
    inx h
    pop d
    call    ?pchar
;  }
    jmp ?4
?6:
;}
?1:
    ret

Понятно, что много вопросов к компилятору, но в целом, Intel 8080 не очень удобен для компилятора С: деления/умножения нет, косвенной адресации через стек тоже нет и т.д.

Ладно, вернемся к Форту. В процессе обдумывания применения Форта для I8080 я написал удобный макро-ассемблер (но об этом будет отдельный пост) и попутно вспомнил об одном своем старом проекте времен Фидо: F-CODE. В качестве приема запутывания кода для защиты от отладчика я реализовывал мини-ядро Форта с прямым шитым кодом.

“Реализовывал мини-ядро”, конечно, звучит, круто, но в реальности интерпретатор шитого кода просто тривиален:

; F-Code Address Interpreter

GetNext$:       cld
                mov     si, IP$
                lodsw
                mov     IP$, si
                retn

CALLR$:         add     RP$, 2
                mov     bp, RP$
                mov     ax, IP$
                mov     [bp], ax
                pop     word ptr IP$
                next

RETR$:          mov     bp, RP$
                mov     ax, [bp]
                mov     IP$, ax
                sub     RP$, 2
                next

NEXT$:          call    GetNext$
                jmp     ax

osPush$:        call    GetNext$
                push    ax
                next

NEXT            MACRO
                jmp     NEXT$
                ENDM

Плюс несколько примитивов, реализованных также на ассемблере:

; Adc  ( a b -> c isCarry )
; if a+b>FFFF isCarry = FFFF else isCarry=0

osAdc$:         pop     ax  dx          ; -> a b
                add     ax, dx
                sbb     dx, dx
                push    ax  dx          ; c isCarry ->
                NEXT

; osSwap ( a b -> b a )

osSwap$:        pop      ax bx
                push     ax bx
                NEXT

; osRot ( a b c -> b c a )

osRot$:         pop      ax bx cx
                push     bx ax cx
                NEXT

osPut$:         add     RP$, 2
                mov     bp, RP$
                pop     word ptr [bp]
                NEXT

osGet$:         mov     bp, RP$
                push    word ptr [bp]
                sub     RP$, 2
                NEXT

osDrop$:        add     sp, 2
                NEXT

; osNor ( a b -> a NOR b )

osNor$:         pop     ax bx
                or      ax, bx
                not     ax
                push    ax
                NEXT

osTrap$:        int     3
                NEXT

; osPeek ( addr -> value )

osPeek$:        pop     bx
                push    word ptr [bx]
                NEXT

; osPoke ( Value Addr -> )

osPoke$:        pop     bx              ; -> Value Addr
                pop     word ptr [bx]   ; ->
                NEXT

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

Вот, например, код для вычисления CRC на данной стековой машине:

CalcCRC:        CALLR                 ; ->
                ofPush  0             ; CRC
                ofPush  0             ; CRC 0
                ofPeekb Buffer+1      ; CRC 0 Size
                $For                  ; CRC
                    osI                   ; CRC i
                    ofPush  Buffer+2      ; CRC i Buffer+2
                    osAdd                 ; CRC Addr
                    osPeekb               ; CRC Value
                    osExch                ; CRC Value*256
                    $For    0, 8          ; CRC Value
                        osShl                 ; CRC Value*2 isCarry
                        osRot                 ; Value*2 isCarry CRC
                        osSwap                ; Value*2 CRC isCarry
                        osRcl                 ; Value*2 CRC*2 isCarry
                        $If <>0               ; Value*2 CRC*2
                            ofXor 8408h           ; Value*2 CRC*2^Const
                        $Endif
                        osSwap                ; CRC*2 Value*2
                    $Loop                   ; CRC Value*2
                    osDrop                ; CRC
                $Loop                 ; CRC
                RETR

Красиво?

В процессе работы над F-CODE родился примитивный препроцессор для ассемблера, позволявший писать код типа:

 lea dx, msg2
 cmp bh, 3
 $if <>0
   lea dx, msg1
 $else
   hlt
 $endif

 cmp dx, 0C0DEh
 $if =0
   lea dx, msg2
 $endif

 mov cx, 2
 $Do
   $Do
   cmp ax, 1
   $EndDo =
   dec cx
 $EndDo Loop

 Store ds, si, ax
     $Do
       cmp al, 1
       $if <>0
         $ExitDo
       $endif
       Store ax, bx, cx, es, bp
         ...
       Restore
       $ContDo
     $EndDo Loop
 Restore

Как и все утилиты во времена ДОС, препроцессор был написан на старом добром Турбо Паскале.

Понятно, что проект имеет чисто исторический интерес, хотя ничто не мешает реализовать интерпретатор Форта хоть на JavaScript’е, и использовать все уже готовые примитивы как есть.

Весь проект F-CODE лежит на GitHub’е – https://github.com/begoon/fcode. Для сборки нужны TASM/TLINK и Турбо Паскаль для препроцессора. Очевидно, что надо все делать в ДОСе.

P.S. При всей низкоуровневости, народ пишет на Форте весьма кучерявые программы. Например, nnbackup, написан на Форте.


Disclaimer

Комментарии