Bootloader golf: Brainf ***

20

Utwórz bootloader, który wykonuje dany program Brainfuck. To jest , więc wygrywa program z najmniejszą liczbą bajtów. Będąc bootloaderem, rozmiar programu jest liczony w niezerowych bajtach w skompilowanym kodzie.

Brainfuck

30000 8-bitowych przepełnionych komórek. Wskaźnik się zawija.

Kilka uwag na temat operacji:

  • Dane wejściowe należy odczytać w taki sposób, aby wszystkie drukowalne znaki ASCII były poprawnie obsługiwane. Inne naciśnięcia klawiszy mogą wstawić dowolny znak lub nic nie robić.
  • Odczytywanie danych wejściowych użytkownika musi być buforowane znakowo, a nie buforowane wierszowo.
  • Odczytywanie danych wprowadzanych przez użytkownika musi zawierać echo wstawionego znaku.
  • Dane wyjściowe muszą być zgodne ze stroną kodową 437 lub domyślną stroną kodową wbudowanych adapterów VGA.

Program rozruchowy

To jest bootloader x86. Program ładujący kończy się tradycyjną 55 AAsekwencją. Twój kod musi działać na VirtualBox, Qemu lub innym dobrze znanym emulatorze x86.

Dysk

Plik wykonywalny Brainfuck znajduje się w drugim sektorze dysku, zaraz za bootloaderem, który, jak zwykle, znajduje się w sekcji MBR, pierwszym sektorze na dysku. Dodatkowy kod (dowolny kod powyżej 510 bajtów) może znajdować się w innych sektorach dysku. Twoje urządzenie magazynujące musi być dyskiem twardym lub dyskietką.

STDIO

Oczywiście bootloader nie może mieć dostępu do możliwości IO systemu operacyjnego. Dlatego funkcje BIOS są używane zamiast do drukowania tekstu i czytania danych wprowadzanych przez użytkownika.

Szablon

Aby rozpocząć, oto prosty szablon napisany w asemblerze Nasm (intel syntax):

[BITS 16]
[ORG 0x7c00]

; first sector:

boot:
    ; initialize segment registers
    xor ax, ax
    mov ds, ax
    mov es, ax
    mov ss, ax

    ; initialize stack
    mov sp, 0x7bfe

    ; load brainfuck code into 0x8000
    ; no error checking is used
    mov ah, 2       ; read
    mov al, 1       ; one sector
    mov ch, 0       ; cylinder & 0xff
    mov cl, 2       ; sector | ((cylinder >> 2) & 0xc0)
    mov dh, 0       ; head
                    ; dl is already the drive number
    mov bx, 0x8000  ; read buffer (es:bx)
    int 0x13        ; read sectors


; fill sector
times (0x200 - 2)-($-$$) db 0

; boot signature
db 0x55, 0xaa

; second sector:

db 'brainfuck code here'

times 0x400-($-$$) db 0

Kompilacja tego jest dość łatwa:

nasm file.asm -f bin -o boot.raw

I uruchom to. Na przykład za pomocą Qemu:

qemu-system-i386 -fda boot.raw

Informacje dodatkowe: wiki OsDev , Wikipedia

Hannes Karppila
źródło
21
Input must be redJestem pewien, że większość programów ładujących natywnie nie obsługuje kolorów.
Pozew Fund Moniki w
4
Młodszym programistom należy wyjaśnić, czym jest dyskietka!
bobbel
1
@bbelbel Zacznijmy od bootloadera
Bálint
5
Nie dlatego, że uważam ten tytuł za obraźliwy, ale jak to możliwe? Według meta nie można w tytule wpisać „bzdura”.
DJMcMayhem
Czy możemy użyć ponad 30 000 komórek?
CalculatorFeline

Odpowiedzi:

13

171 bajtów 1

Wooohoooo! Zajęło to pół dnia, ale było fajnie ...

Więc oto jest. Myślę, że jest zgodny ze specyfikacjami (zawijanie wskaźnika komórki, echo znaków na wejściu, czytanie znak po znaku, echo znaków wejściowych, ...) i wydaje się, że faktycznie działa (cóż, nie próbowałem wielu programów , ale biorąc pod uwagę prostotę języka, zasięg nie jest taki zły, tak myślę).

Ograniczenia

Jedna ważna rzecz: jeśli twój program pieprzenia mózgu zawiera inne znaki niż 8 instrukcji pieprzenia mózgu lub jeśli []nie są one dobrze wyważone, to cię zawiedzie, mouhahahaha!

Ponadto program pieprzenia mózgów nie może przekroczyć 512 bajtów (sektor). Ale wydaje się to zgodne, ponieważ mówisz, że „wykonywalny Brainfuck znajduje się w drugim sektorze dysku” .

Ostatni szczegół: nie zainicjowałem jawnie komórek na zero. Wydaje się, że Qemu robi to za mnie i polegam na tym, ale nie wiem, czy zrobiłby to prawdziwy BIOS na prawdziwym komputerze (inicjalizacja zajęłaby jeszcze kilka bajtów więcej).

Kod

(na podstawie twojego szablonu, a przy okazji, dzięki za to nigdy nie spróbowałbym bez niego):

[BITS 16]
[ORG 0x7C00]

%define cellcount 30000 ; you can't actually increase this value much beyond this point...

; first sector:

boot:
    ; initialize segment registers
    xor ax, ax
    mov ss, ax
    mov ds, ax
    mov es, ax
    jmp 0x0000:$+5

    ; initialize stack
    mov sp, 0x7bfe

    ; load brainfuck code into 0x8000
    ; no error checking is used
    mov ah, 2       ; read
    mov al, 1       ; one sector
    mov ch, 0       ; cylinder & 0xff
    mov cl, 2       ; sector | ((cylinder >> 2) & 0xc0)
    mov dh, 0       ; head
                    ; dl is already the drive number
    mov bx, 0x8000  ; read buffer (es:bx)
    int 0x13        ; read sectors

    ; initialize SI (instruction pointer)
    mov si, bx ; 0x8000
    ; initialize DI (data pointer)
    mov bh, 0x82
    mov di, bx ; 0x8200

decode:
    lodsb ; fetch brainfuck instruction character
.theend:
    test al, al ; endless loop on 0x00
    jz .theend
    and ax, 0x0013 ; otherwise, bit shuffling to get opcode id
    shl ax, 4
    shl al, 2
    shr ax, 1
    add ax, getchar ; and compute instruction implementation address
    jmp ax

align 32, db 0

getchar:
    xor ah, ah
    int 0x16
    cmp al, 13
    jne .normal
    mov al, 10 ; "enter" key translated to newline
.normal:
    mov byte [di], al
    push di
    jmp echochar

align 32, db 0

decrementdata:
    dec byte [di]
    jmp decode

align 32, db 0

putchar:
    push di
    mov al, byte [di]
echochar:
    mov ah, 0x0E
    xor bx, bx
    cmp al, 10 ; newline needs additional carriage return
    jne .normal
    mov al, 13
    int 0x10
    mov al, 10
.normal:
    int 0x10
    pop di
    jmp decode

align 32, db 0

incrementdata:
    inc byte [di]
    jmp decode

align 32, db 0

decrementptr:
    dec di
    cmp di, 0x8200 ; pointer wraparound check (really, was that necessary?)
    jge decode
    add di, cellcount
    jmp decode

align 32, db 0

jumpback:
    pop si
    jmp jumpforward

align 32, db 0

incrementptr:
    inc di
    cmp di, 0x8200+cellcount  ; pointer wraparound check
    jl decode
    sub di, cellcount
    jmp decode

align 32, db 0

jumpforward:
    cmp byte [di], 0
    jz .skip
    push si
    jmp decode
.skip:
    xor bx, bx ; bx contains the count of [ ] imbrication
.loop:
    lodsb
    cmp al, '['
    je .inc
    cmp al, ']'
    jne .loop
    test bx, bx
    jz decode
    dec bx
    jmp .loop
.inc:
    inc bx
    jmp .loop

; fill sector
times (0x1FE)-($-$$) db 0

; boot signature
db 0x55, 0xAA

; second sector contains the actual brainfuck program
; currently: "Hello world" followed by a stdin->stdout cat loop

db '++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++.,[.,]'

times 0x400-($-$$) db 0

Wykorzystane sztuczki

Ok, trochę oszukiwałem. Ponieważ powiedziałeś „będąc bootloaderem, rozmiar programu jest liczony w niezerowych bajtach w skompilowanym kodzie” , zmniejszyłem kod, pozwalając na „dziury” między implementacją ośmiu kodów pieprzonych mózgów. W ten sposób nie potrzebuję dużej sekwencji testów, tabeli skoków ani niczego innego: po prostu przeskakuję do „pieprzenia id” kodu (od 0 do 8) pomnożonego przez 32 w celu wykonania instrukcji pieprzenia mózgu (warto zauważyć, że oznacza to, że wykonanie instrukcji nie może zająć więcej niż 32 bajty).

Co więcej, aby pobrać ten „identyfikator opcode” od postaci programu do pieprzenia mózgu, zauważyłem, że konieczne było trochę przetasowania. Rzeczywiście, jeśli weźmiemy pod uwagę bity 0, 1 i 4 znaku opcode, otrzymamy 8 unikalnych kombinacji:

   X  XX
00101100 0x2C , Accept one byte of input, storing its value in the byte at the pointer.
00101101 0x2D - Decrement (decrease by one) the byte at the pointer.
00101110 0x2E . Output the value of the byte at the pointer.
00101011 0x2B + Increment (increase by one) the byte at the pointer.
00111100 0x3C < Decrement the pointer (to point to the next cell to the left).
01011101 0x5D ] Jump back after the corresp [ if data at pointer is nonzero.
00111110 0x3E > Increment the pointer (to point to the next cell to the right).
01011011 0x5B [ Jump forward after the corresp ] if data at pointer is zero.

I, na szczęście, istnieje tak naprawdę jeden kod operacyjny, który wymaga więcej niż 32 bajtów do wdrożenia, ale jest to ostatni (przeskocz do przodu [). Ponieważ jest więcej miejsca, wszystko jest w porządku.

Inna sztuczka: nie wiem, jak działa typowy interpreter pieprzenia mózgów, ale aby zmniejszyć liczbę rzeczy, tak naprawdę nie wdrożyłem go ]jako „Przeskocz po odpowiednim, [jeśli dane na wskaźniku są niezerowe” . Zamiast tego zawsze wracam do odpowiedniej [i [odtąd ponownie stosuję typową implementację (która następnie, ]jeśli zajdzie taka potrzeba , idzie do przodu po raz kolejny). W tym celu za każdym razem, gdy szyfruję a [, umieszczam bieżący „wskaźnik instrukcji pieprzenia mózgu” na stosie przed wykonaniem instrukcji wewnętrznych i kiedy napotykam], Cofam wskaźnik instrukcji. Tak jakby to było wywołanie funkcji. Można zatem teoretycznie przepełnić stos, tworząc wiele wielu zaabsorbowanych pętli, ale nie przy obecnym ograniczeniu 512-bajtowego kodu pieprzenia mózgu.


1. W tym bajty zerowe, które były częścią samego kodu, ale nie te, które były częścią dopełnienia

ciemny
źródło