
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 DebuggerComo 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. Compilamose 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)