Fort Byte Car (e mais) Nativo Americano

imagem

Sim, sim, é o "byte" e está em indiano (não indiano). Vou começar em ordem. Recentemente aqui, em Habré, começaram a aparecer artigos sobre bytecode. E uma vez eu me diverti escrevendo sistemas Fort. Claro, em assembler. Eles eram de 16 bits. Eu nunca programei no x86-64. Mesmo com 32 não podia jogar. Então surgiu o pensamento - por que não? Por que não agitar o forte de 64 bits e até com o bytecode? Sim, e no Linux, onde eu também não escrevi nada no sistema.

Eu tenho um servidor doméstico com Linux. Em geral, pesquisei um pouco e descobri que o assembler no Linux se chama GAS e o comando as. Estou conectando via SSH ao servidor, digitando como - sim! Eu já o tenho instalado. Ainda precisa de um vinculador, digite ld - sim! Então, e tente escrever algo interessante no assembler. Sem civilização, apenas uma floresta, como índios reais :) Sem um ambiente de desenvolvimento, apenas uma linha de comando e Midnight Commander. O editor será o Nano, que fica no meu F4 em mc. Como o grupo “Zero” está cantando? Um verdadeiro indiano precisa de apenas uma coisa ... O que mais um verdadeiro indiano precisa? Claro, um depurador. Digitamos gdb - is! Bem, pressione Shift + F4 e pronto!

Arquitetura


Para iniciantes, vamos decidir sobre arquitetura. Com profundidades de bits já determinadas, 64 bits. Nas implementações clássicas do Fort, os dados e o segmento de código são os mesmos. Mas, tentaremos fazer o certo. Teremos apenas o código no segmento de código, os dados no segmento de dados. Como resultado, obtemos um kernel para a plataforma e um código de bytes completamente independente da plataforma.

Vamos tentar fazer a máquina de bytes empilhados mais rápida (mas sem JIT). Portanto, teremos uma tabela contendo 256 endereços - um para cada comando de byte. Menos do que tudo - uma verificação extra, estas são 1-2 instruções do processador. E precisamos rapidamente, sem compromisso.

Pilhas


Geralmente, nas implementações Fort, a pilha de retorno do processador (* SP) é usada como uma pilha de dados e a pilha de retorno do sistema fort é implementada usando outros meios. De fato, nossa máquina será empilhada e o trabalho principal está na pilha de dados. Portanto, vamos fazer o mesmo - o RSP será uma pilha de dados. Bem, deixe a pilha de retorno ser RBP, que também, por padrão, funciona com o segmento de pilha. Assim, teremos três segmentos de memória: um segmento de código, um segmento de dados e um segmento de pilha (ele terá uma pilha de dados e uma pilha de retorno).

Registros


Entro na descrição dos registros x86-64 e oops! Existem até 8 registradores de uso geral adicionais (R8 - R16), comparados aos modos de 32 ou 16 bits ...

Já decidiram que precisarão de RSP e RBP. Ainda precisa de um ponteiro (contador) dos comandos do bytecode. Das operações nesse registro, apenas a leitura da memória é necessária. Os principais registradores (RAX, RBX, RCX, RDX, RSI, RDI) são mais flexíveis, universais, com eles existem muitos comandos especiais. Eles nos serão úteis para várias tarefas e, para o contador de instruções de bytecode, levamos um dos novos registros para mim, seja R8.

Vamos começar


Não tenho experiência em programação no Linux em linguagem assembly. Portanto, para iniciantes, encontraremos o "Olá, mundo" finalizado para entender como o programa inicia e exibe o texto. Inesperadamente, para mim, encontrei opções com uma sintaxe estranha, na qual até a fonte e o receptor são reorganizados. Como se viu, esta é a sintaxe da AT&T, e está principalmente escrita sob GAS. Mas há outra opção de sintaxe, chamada de sintaxe Intel. Pensando, decidi usá-lo da mesma forma. Bem, escreva no início de .intel_syntax noprefix.

Compile e execute "Olá, mundo" para garantir que tudo funcione. Lendo a ajuda e os experimentos, comecei a usar o seguinte comando para compilar:
$ as fort.asm -o fort.o -g -ahlsm >list.txt
Aqui, a opção -o indica o arquivo de resultado, a opção -g instrui a gerar informações de depuração e a opção -ahlsm define o formato da listagem. E eu mantenho a saída na lista, nela você pode ver muitas coisas úteis. Admito que no início do trabalho não fiz a listagem e nem especifiquei a opção -g. Comecei a usar a opção -g após o primeiro uso do depurador e comecei a fazer a listagem após as macros aparecerem no código :)

Depois disso, usamos o vinculador, mas aqui nĂŁo Ă© mais simples:

$ ld forth.o -o forth
Bem, corra!
$ ./forth
Hello, world!

Isso funciona.

Este foi o primeiro quarto.asm (na verdade é 'Hellow, mundo!', É claro)
 .intel_syntax noprefix .section .data msg: .ascii "Hello, world!\n" len = . - msg #  len    .section .text .global _start #     _start: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1 — stdout mov ecx, OFFSET FLAT:msg #     mov edx, len #   int 0x80 #   mov eax, 1 #   № 1 — sys_exit xor ebx, ebx #    0 int 0x80 #   

A propĂłsito, descobri mais tarde que no x86-64 Ă© mais correto usar o syscall para uma chamada do sistema, em vez de int 0x80. A chamada 0x80 Ă© considerada obsoleta para essa arquitetura, embora seja suportada.

Um começo foi feito, e agora ...

Vamos lá!


Como haveria pelo menos algumas especificidades, escreveremos o cĂłdigo de um comando de byte. Que seja a palavra Fort "0", colocando 0 no topo da pilha:

 bcmd_num0: push 0 jmp _next 

No momento em que esse comando é executado, o R8 já aponta para o próximo comando de byte. É necessário lê-lo, aumentar R8, determinar o endereço executável pelo código do comando byte e transferir o controle para ele.

Mas ... qual será a profundidade de bits da tabela de endereços de comando de bytes? Então eu tive que cavar o novo sistema de comando x86-64 para mim. Infelizmente, não encontrei comandos que permitam que você vá para o deslocamento na memória. Portanto, calcule o endereço ou o endereço estará pronto - 64 bits. Não há tempo para calcular, o que significa - 64 bits. Nesse caso, o tamanho da tabela será 256 * 8 = 4096 bytes. Bem, finalmente, codifique a chamada _next:

 _next: movzx rcx, byte ptr [r8] inc r8 jmp [bcmd + rcx*8] # bcmd -   - 

Nada mal, parece-me ... Existem apenas três instruções do processador, ao alternar de um comando de byte para outro.

Na verdade, esses comandos não foram tão fáceis para mim. Eu tive que me aprofundar no sistema de comando 0x86-64 novamente e encontrar um novo comando MOVZX para mim. De fato, este comando converte um valor de 8, 16 ou 32 bits em um registro de 64 bits. Existem duas variantes desse comando: não assinado, onde os dígitos mais altos são preenchidos com zeros e o assinado é o MOVSX. Na versão assinada, o sinal se expande, ou seja, para números positivos, os zeros vão para os dígitos mais altos e, para os negativos, para os dígitos. Essa opção também é útil para o comando lit byte.

A propósito, essa opção é a mais rápida? Talvez alguém sugira ainda mais rápido?

Bem, agora temos uma máquina de bytes que pode executar uma sequência de comandos de bytes e executá-los. É necessário testá-lo na prática, forçar a execução de pelo menos uma equipe. Mas qual? Zero na pilha? Mas aqui você nem sabe o resultado, se você não olhar para a pilha no depurador ... Mas se o programa for iniciado, ele poderá ser concluído :)

Nós escrevemos um comando de tchau que completa o programa e escreve sobre ele, especialmente porque temos “Hellow, world!”.

 bcmd_bye: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1 — stdout mov ecx, offset msg_bye #     mov edx, msg_bye_len #   int 0x80 #   mov eax, 1 #   № 1 — sys_exit mov ebx, 0 #    0 int 0x80 #   

A única coisa que resta é criar uma tabela de endereços de comando de byte, inicializar os registradores e iniciar a máquina de bytes. Então ... a tabela tem 256 valores e existem dois comandos. O que há nas outras células?
O restante terá um código de operação inválido. Mas você não pode verificar, são equipes extras, agora temos três e, com a verificação, serão cinco. Então, faremos um comando como esse - uma equipe ruim. Primeiro, preenchemos a tabela inteira e começamos a ocupar as células com comandos úteis. Deixe o time ruim ter o código 0x00, o time bye - 0x01, e o '0' terá o código 0x02, uma vez que já esteja escrito. Por enquanto, a equipe ruim fará o mesmo que o adeus, apenas com um código e texto de conclusão diferentes (eu o colocarei no spoiler, quase o mesmo que o adeus):

bcmd_bad
 bcmd_bad: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1 — stdout mov ecx, offset msg_bad_byte #     mov edx, msg_bad_byte_len #   int 0x80 #   mov eax, 1 #   № 1 — sys_exit mov ebx, 1 #    1 int 0x80 #   
Agora desenhe uma tabela de endereços. Por conveniência, colocaremos oito em cada linha, haverá 16. A tabela é bastante grande em tamanho:

Tabela de Endereço do Comando Byte
 bcmd: .quad bcmd_bad, bcmd_bye, bcmd_num0, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad 
Nós escrevemos o corpo do programa de bytes. Para fazer isso, atribua códigos de comando às variáveis ​​do assembler. Teremos os seguintes acordos:

  • Os endereços para executar comandos de byte começarĂŁo em bcmd_
  • Os cĂłdigos de comando em si serĂŁo armazenados em variáveis ​​começando com b_

Assim, o corpo do programa de bytes será assim:

 start: .byte b_bye 

Declare o tamanho da pilha de dados como stack_size. Que seja até agora 1024. Na inicialização, faremos RBP = RSP - stack_size.

Na verdade, obtemos um cĂłdigo de programa (adiante.asm)
 .intel_syntax noprefix stack_size = 1024 .section .data msg_bad_byte: .ascii "Bad byte code!\n" msg_bad_byte_len = . - msg_bad_byte #  len    msg_bye: .ascii "bye!\n" msg_bye_len = . - msg_bye msg_hello: .ascii "Hello, world!\n" msg_hello_len = . - msg_hello bcmd: .quad bcmd_bad, bcmd_bye, bcmd_num0, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad start: .byte b_bye .section .text .global _start #     _start: mov rbp, rsp sub rbp, stack_size lea r8, start jmp _next _next: movzx rcx, byte ptr [r8] inc r8 jmp [bcmd + rcx*8] b_bad = 0x00 bcmd_bad: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1 — stdout mov ecx, offset msg_bad_byte #     mov edx, msg_bad_byte_len #   int 0x80 #   mov eax, 1 #   № 1 — sys_exit mov ebx, 1 #    1 int 0x80 #   b_bye = 0x01 bcmd_bye: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1 — stdout mov ecx, offset msg_bye #     mov edx, msg_bye_len #   int 0x80 #   mov eax, 1 #   № 1 — sys_exit mov ebx, 0 #    0 int 0x80 #   b_num0 = 0x02 bcmd_num0: push 0 jmp _next 


Compilar, execute:

$ as fort.asm -o fort.o -g -ahlsm >list.txt
$ ld forth.o -o forth
$ ./forth
bye!

Isso funciona! Nosso primeiro programa de bytecode de um byte foi lançado :)
Obviamente, será assim se tudo for feito corretamente. E se não, então o resultado provavelmente será este:

$ ./forth


Claro, outras opções são possíveis, mas eu me deparei com isso com mais frequência. E precisamos de um depurador.

Letra da mĂşsica Debugger
Como já mencionado, usei o GDB. Este é um depurador bastante poderoso, mas com uma interface de linha de comando. Executá-lo é muito simples:

 $ gdb ./forth GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1 Copyright (C) 2016 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "x86_64-linux-gnu". Type "show configuration" for configuration details. For bug reporting instructions, please see: <http://www.gnu.org/software/gdb/bugs/>. Find the GDB manual and other documentation resources online at: <http://www.gnu.org/software/gdb/documentation/>. For help, type "help". Type "apropos word" to search for commands related to "word"... Reading symbols from ./forth...done. (gdb) 

Em seguida, inserindo comandos, nós depuramos. Tive horas suficientes para encontrar alguns comandos necessários e aprender a usá-los para depuração. Aqui estão elas:
b <rótulo> - define um ponto de interrupção
l <label> - exibir cĂłdigo fonte
r - inicia ou reinicia o programa
ir - visualizar status de registros do processador
s - passo

A propósito, lembre-se de que você precisa compilar o programa com a opção -g? Caso contrário, as tags e o código-fonte não estarão disponíveis. Nesse caso, será possível depurar apenas por código desmontado e usar os endereços na memória. Nós, é claro, somos índios, mas não na mesma medida ...

Mas de alguma forma o programa faz muito pouco. Nós apenas dizemos "Olá" para ela, e ela imediatamente diz "Tchau!". Vamos fazer o verdadeiro "Olá, mundo!" no bytecode. Para fazer isso, coloque o endereço e o comprimento da string na pilha, execute o comando que exibe a string e, em seguida, o comando bye. Para fazer tudo isso, novos comandos são necessários: digite para gerar a sequência e acenda para colocar o endereço e o comprimento da sequência. Primeiro, escrevemos o tipo, deixe seu código ser 0x80. Novamente, precisamos desse pedaço de código com a chamada sys_write:

 b_type = 0x80 bcmd_type: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1 — stdout pop rdx pop rcx push r8 int 0x80 #   pop r8 jmp _next 

Aqui pegamos o endereço e o comprimento da string da pilha de dados usando comandos POP. Chamar int 0x80 pode alterar o registro de R8, então nós o salvamos. Nós não fizemos isso antes porque o programa estava terminando. O conteúdo desses registros não se importava. Agora, este é um comando de byte comum, após o qual o código de byte continua sendo executado e você precisa se comportar.

Agora vamos escrever o lit. Esta será a nossa primeira equipe com parâmetros. Após o byte com o código para este comando, haverá bytes contendo o número que ele colocará na pilha. A pergunta surge imediatamente - que profundidade de bit é necessária aqui? Para colocar qualquer número, você precisa de 64 bits. Mas, cada vez que o comando ocupará 9 bytes, o que colocaria um número? Perdemos a compactação, uma das principais propriedades do bytecode, e o código do forte também ...

A solução é simples - faremos vários comandos para diferentes profundidades de bits. Estes serão lit8, lit16, lit32 e lit64. Para números pequenos, usaremos lit8 e lit16, para números maiores - lit32 e lit64. Números pequenos são mais comumente usados ​​e, para eles, será o comando mais curto, que leva dois bytes. Nada mal! .. Vamos criar os códigos desses comandos 0x08 - 0x0B.

 b_lit8 = 0x08 bcmd_lit8: movsx rax, byte ptr [r8] inc r8 push rax jmp _next b_lit16 = 0x09 bcmd_lit16: movsx rax, word ptr [r8] add r8, 2 push rax jmp _next b_lit32 = 0x0A bcmd_lit32: movsx rax, dword ptr [r8] add r8, 4 push rax jmp _next b_lit64 = 0x0B bcmd_lit64: mov rax, [r8] add r8, 8 push rax jmp _next 

Aqui usamos o comando MOVSX - esta é uma versão icônica do comando MOVZX já conhecido por nós. R8, temos um contador de comandos de bytes. Carregamos nele o valor do tamanho desejado, movemos para o próximo comando e colocamos o valor convertido em 64 bits na pilha.

Não se esqueça de adicionar os endereços das novas equipes na tabela às posições desejadas.

Está tudo pronto para escrever seu primeiro programa "Olá, mundo!" no nosso bytecode. Vamos trabalhar com o compilador! :)

 start: .byte b_lit64 .quad msg_hello .byte b_lit8 .byte msg_hello_len .byte b_type .byte b_bye 

Usamos dois comandos iluminados diferentes: lit64, que colocaria o endereço da string na pilha, e lit8, com o qual colocamos o comprimento na pilha. Em seguida, executamos mais dois comandos de byte: type e bye.
Compilar, execute:

 $ as fort.asm -o fort.o -g -ahlsm >list.txt $ ld forth.o -o forth $ ./forth Hello, world! bye! 

Ganhou nosso bytecode! Este Ă© o resultado que deve ser, se estiver tudo bem.

Fonte completa
 .intel_syntax noprefix stack_size = 1024 .section .data msg_bad_byte: .ascii "Bad byte code!\n" msg_bad_byte_len = . - msg_bad_byte #  len    msg_bye: .ascii "bye!\n" msg_bye_len = . - msg_bye msg_hello: .ascii "Hello, world!\n" msg_hello_len = . - msg_hello bcmd: .quad bcmd_bad, bcmd_bye, bcmd_num0, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x00 .quad bcmd_lit8, bcmd_lit16, bcmd_lit32, bcmd_lit64, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x10 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x20 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x30 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x40 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x60 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_type, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x80 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad start: .byte b_lit64 .quad msg_hello .byte b_lit8 .byte msg_hello_len .byte b_type .byte b_bye .section .text .global _start #     _start: mov rbp, rsp sub rbp, stack_size lea r8, start jmp _next _next: movzx rcx, byte ptr [r8] inc r8 jmp [bcmd + rcx*8] b_bad = 0x00 bcmd_bad: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1 — stdout mov ecx, offset msg_bad_byte #     mov edx, msg_bad_byte_len #   int 0x80 #   mov eax, 1 #   № 1 — sys_exit mov ebx, 1 #    1 int 0x80 #   b_bye = 0x01 bcmd_bye: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1 — stdout mov ecx, offset msg_bye #     mov edx, msg_bye_len #   int 0x80 #   mov eax, 1 #   № 1 — sys_exit mov ebx, 0 #    0 int 0x80 #   b_num0 = 0x02 bcmd_num0: push 0 jmp _next b_lit8 = 0x08 bcmd_lit8: movsx rax, byte ptr [r8] inc r8 push rax jmp _next b_lit16 = 0x09 bcmd_lit16: movsx rax, word ptr [r8] add r8, 2 push rax jmp _next b_lit32 = 0x0A bcmd_lit32: movsx rax, dword ptr [r8] add r8, 4 push rax jmp _next b_lit64 = 0x0B bcmd_lit64: mov rax, [r8] add r8, 8 push rax jmp _next b_type = 0x80 bcmd_type: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1 — stdout pop rdx pop rcx push r8 int 0x80 #   pop r8 jmp _next 


Mas as possibilidades ainda são muito primitivas, você não pode criar uma condição, um ciclo.

Como não? Você pode, tudo está em nossas mãos! Vamos fazer essa linha no loop 10 vezes. Isso exigirá um comando de ramificação condicional, além de um pouco de aritmética da pilha: um comando que diminua o valor na pilha em 1 (no forte “1-”) e um comando de duplicação de vértices (“dup”).

Com aritmética, tudo é simples, nem vou comentar:

 b_dup = 0x18 bcmd_dup: push [rsp] jmp _next b_wm = 0x20 bcmd_wm: decq [rsp] jmp _next 

Agora um salto condicional. Para iniciantes, vamos simplificar a tarefa - uma transição incondicional. É claro que você só precisa alterar o valor do registro R8. A primeira coisa que vem à mente é um comando byte, seguido por um parâmetro - o endereço de transição é de 64 bits. Novamente nove bytes. Precisamos desses nove bytes? As transições geralmente ocorrem em distâncias curtas, geralmente dentro de algumas centenas de bytes. Portanto, não usaremos o endereço, mas o deslocamento!

Profundidade de bits? Em muitos casos, 8 bits (127 para frente / trás) serão suficientes, mas às vezes isso não será suficiente. Portanto, faremos o mesmo que com o comando aceso, faremos duas opções - 8 e 16 dígitos, os códigos de comando serão 0x10 e 0x11:

 b_branch8 = 0x10 bcmd_branch8: movsx rax, byte ptr [r8] add r8, rax jmp _next b_branch16 = 0x11 bcmd_branch16: movsx rax, word ptr [r8] add r8, rax jmp _next 
Agora a transição condicional é fácil de implementar. Se a pilha for 0, vá para _next e, se não, vá para o comando branch!
 b_qbranch8 = 0x12 bcmd_qbranch8: pop rax or rax, rax jnz bcmd_branch8 inc r8 jmp _next b_qbranch16 = 0x13 bcmd_qbranch16: pop rax or rax, rax jnz bcmd_branch16 add r8, 2 jmp _next 
Agora temos tudo para fazer um loop:
 start: .byte b_lit8 .byte 10 #  #  m0: .byte b_lit64 .quad msg_hello .byte b_lit8 .byte msg_hello_len .byte b_type .byte b_wm .byte b_dup .byte b_qbranch8 .byte m0 - . .byte b_bye 

Os dois primeiros comandos - colocamos o contador de loops na pilha. Em seguida, imprima a sequência Olá. Subtraímos 1 do contador, duplicamos e realizamos (ou não realizamos) a transição. O comando de duplicação é necessário porque o comando de ramificação condicional assume o valor da parte superior da pilha. A transição aqui é de oito bits, pois a distância é de apenas alguns bytes.

Colocamos os endereços de novos comandos em uma tabela, compilamos e executamos.

Vou colocar em um spoiler, caso contrário, nosso programa se tornou detalhado)
 $ as fort.asm -o fort.o -g -ahlsm >list.txt $ ld forth.o -o forth $ ./forth Hello, world! Hello, world! Hello, world! Hello, world! Hello, world! Hello, world! Hello, world! Hello, world! Hello, world! Hello, world! bye! 


Bem, já podemos fazer condições e ciclos!

Fonte completa
 .intel_syntax noprefix stack_size = 1024 .section .data msg_bad_byte: .ascii "Bad byte code!\n" msg_bad_byte_len = . - msg_bad_byte #  len    msg_bye: .ascii "bye!\n" msg_bye_len = . - msg_bye msg_hello: .ascii "Hello, world!\n" msg_hello_len = . - msg_hello bcmd: .quad bcmd_bad, bcmd_bye, bcmd_num0, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x00 .quad bcmd_lit8, bcmd_lit16, bcmd_lit32, bcmd_lit64, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_branch8, bcmd_branch16, bcmd_qbranch8, bcmd_qbranch16, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x10 .quad bcmd_dup, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_wm, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x20 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x30 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x40 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x60 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_type, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x80 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad start: .byte b_lit8 .byte 10 #  #  m0: .byte b_lit64 .quad msg_hello .byte b_lit8 .byte msg_hello_len .byte b_type .byte b_wm .byte b_dup .byte b_qbranch8 .byte m0 - . .byte b_bye .section .text .global _start #     _start: mov rbp, rsp sub rbp, stack_size lea r8, start jmp _next _next: movzx rcx, byte ptr [r8] inc r8 jmp [bcmd + rcx*8] b_bad = 0x00 bcmd_bad: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1 — stdout mov ecx, offset msg_bad_byte #     mov edx, msg_bad_byte_len #   int 0x80 #   mov eax, 1 #   № 1 — sys_exit mov ebx, 1 #    1 int 0x80 #   b_bye = 0x01 bcmd_bye: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1 — stdout mov ecx, offset msg_bye #     mov edx, msg_bye_len #   int 0x80 #   mov eax, 1 #   № 1 — sys_exit mov ebx, 0 #    0 int 0x80 #   b_num0 = 0x02 bcmd_num0: push 0 jmp _next b_lit8 = 0x08 bcmd_lit8: movsx rax, byte ptr [r8] inc r8 push rax jmp _next b_lit16 = 0x09 bcmd_lit16: movsx rax, word ptr [r8] add r8, 2 push rax jmp _next b_lit32 = 0x0A bcmd_lit32: movsx rax, dword ptr [r8] add r8, 4 push rax jmp _next b_lit64 = 0x0B bcmd_lit64: mov rax, [r8] add r8, 8 push rax jmp _next b_type = 0x80 bcmd_type: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1 — stdout pop rdx pop rcx push r8 int 0x80 #   pop r8 jmp _next b_dup = 0x18 bcmd_dup: push [rsp] jmp _next b_wm = 0x20 bcmd_wm: decq [rsp] jmp _next b_branch8 = 0x10 bcmd_branch8: movsx rax, byte ptr [r8] add r8, rax jmp _next b_branch16 = 0x11 bcmd_branch16: movsx rax, word ptr [r8] add r8, rax jmp _next b_qbranch8 = 0x12 bcmd_qbranch8: pop rax or rax, rax jnz bcmd_branch8 inc r8 jmp _next b_qbranch16 = 0x13 bcmd_qbranch16: pop rax or rax, rax jnz bcmd_branch16 add r8, 2 jmp _next 

Mas até que a máquina de bytes concluída esteja faltando outra função muito importante. Não podemos chamar outro do bytecode. Não temos o que chamamos de rotinas, procedimentos etc. E no forte, sem isso, não podemos usar outras palavras além das palavras do núcleo em algumas palavras.

Trazemos o trabalho até o fim. Aqui, pela primeira vez, precisamos de uma pilha de retornos. São necessários dois comandos - o comando de chamada e o comando de retorno (chamar e sair).

O comando call, em princípio, faz o mesmo que o branch - transfere o controle para outro pedaço de bytecode. Mas, diferentemente da ramificação, você ainda precisa salvar o endereço de retorno na pilha de retorno para poder retornar e continuar a execução. Há outra diferença - essas chamadas podem ocorrer a distâncias muito maiores. Portanto, fazemos o comando call à semelhança de branch, mas em três versões - 8, 16 e 32 bits.

 b_call8 = 0x0C bcmd_call8: movsx rax, byte ptr [r8] sub rbp, 8 inc r8 mov [rbp], r8 add r8, rax jmp _next b_call16 = 0x0D bcmd_call16: movsx rax, word ptr [r8] sub rbp, 8 add r8, 2 mov [rbp], r8 add r8, rax jmp _next b_call32 = 0x0E bcmd_call32: movsx rax, dword ptr [r8] sub rbp, 8 add r8, 4 mov [rbp], r8 add r8, rax jmp _next 

Como você pode ver, aqui, diferentemente das transições, três equipes são adicionadas. Um deles reorganiza R8 para o próximo comando de byte e os dois restantes armazenam o valor recebido na pilha de retorno. A propósito, aqui tentei não colocar as instruções do processador dependentes uma da outra lado a lado, para que o transportador do processador pudesse executar os comandos em paralelo. Mas não sei o quanto isso causa algum efeito. Se desejar, você pode verificar os testes.

Deve-se ter em mente que a formação de um argumento para o comando de chamada é um pouco diferente do que para o ramo. Para ramificação, o deslocamento é calculado como a diferença entre o endereço da ramificação e o endereço do byte após o comando byte. E para o comando de chamada, esta é a diferença entre o endereço de salto e o endereço do próximo comando. Por que isso é necessário?Isso resulta em menos instruções do processador.

Agora, o comando de retorno. Na verdade, seu trabalho é apenas restaurar o R8 da pilha de retorno e transferir o controle para a máquina de bytes:

 b_exit = 0x1F bcmd_exit: mov r8, [rbp] add rbp, 8 jmp _next 

Esses comandos serão usados ​​com muita frequência e precisam ser otimizados ao máximo. O comando exit byte ocupa três instruções da máquina. É possível reduzir algo aqui? Acontece que você pode! Você pode simplesmente remover o comando de transição :)

Para fazer isso, coloque-o acima do ponto de entrada da máquina _next byte:

 b_exit = 0x1F bcmd_exit: mov r8, [rbp] add rbp, 8 _next: movzx rcx, byte ptr [r8] inc r8 jmp [bcmd + rcx*8] 

A propósito, os comandos mais importantes e usados ​​com mais freqüência (por exemplo, como chamada) precisam ser colocados mais próximos à máquina de bytes, para que o compilador possa formar um comando de salto curto. Isso é claramente visível na listagem. Aqui está um exemplo.

  262 0084 490FBE00 bcmd_lit8: movsx rax, byte ptr [r8] 263 0088 49FFC0 inc r8 264 008b 50 push rax 265 008c EB90 jmp _next 266 267 b_lit16 = 0x09 268 008e 490FBF00 bcmd_lit16: movsx rax, word ptr [r8] 269 0092 4983C002 add r8, 2 270 0096 50 push rax 271 0097 EB85 jmp _next 272 273 b_lit32 = 0x0A 274 0099 496300 bcmd_lit32: movsx rax, dword ptr [r8] 275 009c 4983C004 add r8, 4 276 00a0 50 push rax 277 00a1 E978FFFF jmp _next 277 FF 278 

Aqui, nas linhas 265 e 271, o comando jmp ocupa 2 bytes cada e na linha 277, o mesmo comando é compilado em 5 bytes, pois a distância do salto excedeu o comprimento do comando curto.

Portanto, comandos de byte, como tipo ruim, adeus, são reorganizados ainda mais e, como chamada, ramificação, aceso, estão mais próximos. Infelizmente, não há muito que possa caber em uma transição de 127 bytes.
Adicionamos novos comandos à tabela de endereços de acordo com seus códigos.

Então, agora temos um desafio e um retorno, vamos testá-los! Para fazer isso, selecione a impressão da linha em um procedimento separado e a chamaremos duas vezes. E o número de repetições do ciclo é reduzido para três.

 start: .byte b_lit8 .byte 3 #  #  m0: .byte b_call16 .word sub_hello - . - 2 .byte b_call16 .word sub_hello - . - 2 .byte b_wm .byte b_dup .byte b_qbranch8 .byte m0 - . .byte b_bye sub_hello: .byte b_lit64 .quad msg_hello .byte b_lit8 .byte msg_hello_len .byte b_type .byte b_exit 

O Call8 pode ser usado aqui, mas decidi usar o call16 como o mais provável. O valor 2 é subtraído devido às peculiaridades do cálculo do endereço do comando de chamada de byte que escrevi sobre. Para call8, 1 será deduzido aqui, para call32, respectivamente, 4. Compilamos
e chamamos:

 $ as forth.asm -o forth.o -g -ahlsm>list.txt $ ld forth.o -o forth $ ./forth Hello, world! Bad byte code! 

Opa ... como se costuma dizer, algo deu errado :) Bem, lançamos o GDB e vemos o que acontece lá. Defino um ponto de interrupção imediatamente em bcmd_exit, pois fica claro que a chamada sub_hello está passando e o corpo da rotina é executado ... iniciado ... e o programa não atingiu o ponto de interrupção. Imediatamente houve uma suspeita de um código de comando de bytes. E, de fato, a razão estava nele. b_exit Atribuí o valor 0x1f e o próprio endereço foi colocado no número de célula da tabela 0x17. Bem, vou corrigir o valor de b_exit para 0x17 e tentar novamente:

 $ as forth.asm -o forth.o -g -ahlsm>list.txt $ ld forth.o -o forth $ ./forth Hello, world! Hello, world! Hello, world! Hello, world! Hello, world! Hello, world! bye! 

Exatamente seis vezes a saudação e uma vez adeus. Como deveria ser :)

Fonte completa
 .intel_syntax noprefix stack_size = 1024 .section .data msg_bad_byte: .ascii "Bad byte code!\n" msg_bad_byte_len = . - msg_bad_byte #  len    msg_bye: .ascii "bye!\n" msg_bye_len = . - msg_bye msg_hello: .ascii "Hello, world!\n" msg_hello_len = . - msg_hello bcmd: .quad bcmd_bad, bcmd_bye, bcmd_num0, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x00 .quad bcmd_lit8, bcmd_lit16, bcmd_lit32, bcmd_lit64, bcmd_call8, bcmd_call16, bcmd_call32, bcmd_bad .quad bcmd_branch8, bcmd_branch16, bcmd_qbranch8, bcmd_qbranch16, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_exit # 0x10 .quad bcmd_dup, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_wm, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x20 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x30 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x40 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x60 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_type, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # 0x80 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad start: .byte b_lit8 .byte 3 #  #  m0: .byte b_call16 .word sub_hello - . - 2 .byte b_call16 .word sub_hello - . - 2 .byte b_wm .byte b_dup .byte b_qbranch8 .byte m0 - . .byte b_bye sub_hello: .byte b_lit64 .quad msg_hello .byte b_lit8 .byte msg_hello_len .byte b_type .byte b_exit .section .text .global _start #     _start: mov rbp, rsp sub rbp, stack_size lea r8, start jmp _next b_exit = 0x17 bcmd_exit: mov r8, [rbp] add rbp, 8 _next: movzx rcx, byte ptr [r8] inc r8 jmp [bcmd + rcx*8] b_num0 = 0x02 bcmd_num0: push 0 jmp _next b_lit8 = 0x08 bcmd_lit8: movsx rax, byte ptr [r8] inc r8 push rax jmp _next b_lit16 = 0x09 bcmd_lit16: movsx rax, word ptr [r8] add r8, 2 push rax jmp _next b_call8 = 0x0C bcmd_call8: movsx rax, byte ptr [r8] sub rbp, 8 inc r8 mov [rbp], r8 add r8, rax jmp _next b_call16 = 0x0D bcmd_call16: movsx rax, word ptr [r8] sub rbp, 8 add r8, 2 mov [rbp], r8 add r8, rax jmp _next b_call32 = 0x0E bcmd_call32: movsx rax, dword ptr [r8] sub rbp, 8 add r8, 4 mov [rbp], r8 add r8, rax jmp _next b_lit32 = 0x0A bcmd_lit32: movsx rax, dword ptr [r8] add r8, 4 push rax jmp _next b_lit64 = 0x0B bcmd_lit64: mov rax, [r8] add r8, 8 push rax jmp _next b_dup = 0x18 bcmd_dup: push [rsp] jmp _next b_wm = 0x20 bcmd_wm: decq [rsp] jmp _next b_branch8 = 0x10 bcmd_branch8: movsx rax, byte ptr [r8] add r8, rax jmp _next b_branch16 = 0x11 bcmd_branch16: movsx rax, word ptr [r8] add r8, rax jmp _next b_qbranch8 = 0x12 bcmd_qbranch8: pop rax or rax, rax jnz bcmd_branch8 inc r8 jmp _next b_qbranch16 = 0x13 bcmd_qbranch16: pop rax or rax, rax jnz bcmd_branch16 add r8, 2 jmp _next b_bad = 0x00 bcmd_bad: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1 — stdout mov ecx, offset msg_bad_byte #     mov edx, msg_bad_byte_len #   int 0x80 #   mov eax, 1 #   № 1 — sys_exit mov ebx, 1 #    1 int 0x80 #   b_bye = 0x01 bcmd_bye: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1 — stdout mov ecx, offset msg_bye #     mov edx, msg_bye_len #   int 0x80 #   mov eax, 1 #   № 1 — sys_exit mov ebx, 0 #    0 int 0x80 #   b_type = 0x80 bcmd_type: mov eax, 4 #   № 4 — sys_write mov ebx, 1 #  № 1 — stdout pop rdx pop rcx push r8 int 0x80 #   pop r8 jmp _next 


Qual Ă© o resultado


Fizemos e testamos uma máquina de byte de pilha de 64 bits completa e bastante rápida. Em velocidade, talvez essa máquina de bytes seja uma das mais rápidas da sua classe (uma máquina de bytes de pilha sem JIT). Ela sabe como executar comandos sequencialmente, fazer saltos condicionais e incondicionais, chamar procedimentos e retornar a partir deles. Ao mesmo tempo, o bytecode usado é razoavelmente compacto. Basicamente, os comandos de bytes levam de 1 a 3 bytes, mais é muito raro (apenas números grandes e chamadas de procedimento muito distantes). Também é esboçado um pequeno conjunto de comandos de bytes, que é fácil de expandir. Suponha que todos os comandos básicos para trabalhar com a pilha (drop, swap, over, root, etc. possam ser escritos em 20 minutos, a mesma quantidade será aplicada a comandos inteiros aritméticos).

Outro ponto importante. O bytecode, diferentemente do código forte clássico de costura direta, não contém instruções da máquina, portanto pode ser transferido sem recompilação para outra plataforma. É suficiente reescrever o kernel uma vez no sistema de instruções do novo processador, e isso pode ser feito muito rapidamente.

A versão atual da máquina de bytes não é específica para nenhum idioma específico. Mas quero fazer a implementação da linguagem Fort, porque tenho experiência com ela, e o compilador para isso pode ser feito muito rapidamente.

Se houver interesse nisso, com base nesta máquina, no próximo artigo, farei entrada e saída de strings e números, um dicionário forte e um intérprete. Você pode "tocar" a equipe com as mãos. Bem, no terceiro artigo, criaremos um compilador e obteremos um sistema quase completo de fort. Em seguida, será possível escrever e compilar alguns algoritmos padrão e comparar o desempenho com outros idiomas e sistemas. Você pode usar, por exemplo, a peneira de Eratóstenes e similares.

É interessante experimentar opções. Por exemplo, torne a tabela de comandos 16 bits e veja como isso afetará o desempenho. Você também pode transformar o ponto de entrada _next em uma macro. Nesse caso, o código de máquina de cada comando de byte aumentará de tamanho em dois comandos (menos a transição e mais três comandos de _next). Ou seja, no final, não haverá transição para _next, mas o conteúdo do _next aponta em si (14 bytes). É interessante saber como isso afetará o desempenho. Você também pode tentar otimizar usando registros. Por exemplo, um loop padrão com um contador no forte armazena o contador na pilha de retorno. Você pode criar uma versão de registro e também testá-la.

Você também pode criar um compilador de expressões escritas na forma clássica (por exemplo, A = 5 + (B + C * 4)).

Em geral, há espaço para experimentação! :)

Continuação: Byte-machine para o forte (e não apenas) no nativo americano (parte 2)

Source: https://habr.com/ru/post/pt431932/


All Articles