
Chegou o ano de 2019. As férias de ano novo estão chegando ao fim. É hora de começar a lembrar de bytes, comandos, variáveis, loops ...
Algo que eu já esqueci com essas férias. Tem que lembrar juntos!
Hoje vamos criar um intérprete para nossa máquina de bytes. Este é o terceiro artigo, as primeiras partes estão aqui:
parte 1 ,
parte 2 .
Feliz Ano Novo para todos, e bem-vindo ao corte!
Para começar, responderei às perguntas do
fpauk . Essas perguntas estão absolutamente corretas. Agora a arquitetura desta máquina de bytes é tal que trabalhamos com endereços diretos do processador. Mas no bytecode esses endereços não são, eles são formados após o início do sistema. Após o início do sistema, podemos criar qualquer ponteiro, e esse código funcionará corretamente em qualquer plataforma. Por exemplo, o endereço de uma variável ou matriz pode ser obtido com o comando var0. Este comando funcionará em qualquer plataforma e retornará o endereço correto específico para esta plataforma. Então você pode trabalhar com este endereço como quiser.
Mas ainda assim,
fpauk está certo. O endereço não pode ser armazenado no bytecode. Acontece que podemos escrever código independente de plataforma, mas para isso precisamos fazer alguns esforços. Em particular, verifique se os endereços não estão no bytecode. E eles podem entrar, por exemplo, se você salvar o código compilado em um arquivo. Ele conterá dados e pode ser endereços. Por exemplo, os valores das variáveis aqui, contexto e outras.
Para se livrar desse problema, você precisa tornar os endereços virtuais. O endereçamento do processador x86 é bastante poderoso e, na maioria dos casos, nem adiciona comandos extras. Mas ainda assim, continuarei na arquitetura atual, com endereços absolutos. E então, quando chegarmos aos testes, será possível refazer os endereços em endereços virtuais e ver como isso afetará o desempenho. Isso é interessante.
Aquecer
E agora um pouco de treino. Vamos criar outra parte dos comandos de bytes pequenos, mas úteis. Esses serão os comandos nip, emit, 1+, +!, -!, Count, palavras de trabalho com a pilha de retorno r>,> r, r @, uma string literal (") e palavras constantes 1, 2, 3, 4, 8. Não esqueça de incluí-los na tabela de comandos.
Aqui está o código para esses comandosb_nip = 0x39 bcmd_nip: pop rax mov [rsp], rax jmp _next b_emit = 0x81 bcmd_emit: pop rax mov rsi, offset emit_buf # mov [rsi], al mov rax, 1 # № 1 - sys_write mov rdi, 1 # № 1 - stdout mov rdx, 1 # push r8 syscall # pop r8 jmp _next b_wp = 0x18 bcmd_wp: incq [rsp] jmp _next b_setp = 0x48 bcmd_setp: pop rcx pop rax add [rcx], rax jmp _next b_setm = 0x49 bcmd_setm: pop rcx pop rax sub [rcx], rax jmp _next b_2r = 0x60 bcmd_2r: pop rax sub rbp, 8 mov [rbp], rax jmp _next b_r2 = 0x61 bcmd_r2: push [rbp] add rbp, 8 jmp _next b_rget = 0x62 bcmd_rget: push [rbp] jmp _next b_str = 0x82 bcmd_str: movzx rax, byte ptr [r8] lea r8, [r8 + rax + 1] jmp _next b_count = 0x84 bcmd_count: pop rcx movzx rax, byte ptr [rcx] inc rcx push rcx push rax jmp _next b_num1 = 0x03 bcmd_num1: push 1 jmp _next b_num2 = 0x04 bcmd_num2: push 2 jmp _next b_num3 = 0x05 bcmd_num3: push 3 jmp _next b_num4 = 0x06 bcmd_num4: push 4 jmp _next b_num8 = 0x07 bcmd_num8: push 8 jmp _next
O comando nip remove a palavra abaixo da parte superior da pilha. É equivalente a trocar comandos drop. Às vezes, isso pode ser útil.
O comando emit empurra um caractere da pilha. Ele usa o mesmo número de chamada do sistema 1, o caractere coloca em um buffer com o comprimento 1.
O comando count é muito simples - pega o endereço da linha com o contador da pilha e o transforma em dois valores - o endereço da linha sem o contador e o comprimento.
Os comandos b_2r, b_r2, b_rget são as palavras Fort r>,> r, r @. O primeiro pega a palavra da pilha de retorno e a coloca na pilha aritmética. O segundo realiza a operação oposta. O terceiro copia a palavra da pilha de retorno, coloca-a na aritmética, a pilha de retorno não muda.
Os comandos b_setp e b_setm são as palavras +! e -! .. Eles pegam o valor e o endereço da pilha e modificam a palavra no endereço especificado, adicionando ou removendo o valor da pilha.
O comando b_str possui um parâmetro de comprimento arbitrário - uma linha com um contador. Essa linha está no bytecode após o byte de comando e o comando simplesmente envia o endereço dessa linha para a pilha. De fato, esta é uma string literal.
O resto da equipe, acho, não precisa de comentários.
Também criaremos um comando para imprimir uma string constante (. "). Vamos implementá-lo como um ponto de entrada para digitar, da seguinte maneira:
b_strp = 0x83 bcmd_strp: movsx rax, byte ptr [r8] inc r8 push rax push r8 add r8, rax b_type = 0x80 bcmd_type: mov rax, 1 # № 1 - sys_write mov rdi, 1 # № 1 - stdout pop rdx # pop rsi # push r8 syscall # pop r8 jmp _next
Este comando está estruturado de forma semelhante ao b_str. Só que ela não coloca nada na pilha. A linha localizada atrás deste comando como parâmetro é simplesmente exibida ao usuário.
O aquecimento terminou, chegou a hora de algo mais sério. Vamos lidar com os geradores de palavras e outros comandos var.
Palavras geradoras
Lembre-se das variáveis. Sabemos como eles são organizados no nível do bytecode (comando var0). Para criar uma nova variável, o forte usa a seguinte construção:
variable < >
Após executar esta sequência, uma nova palavra <nome da variável> é criada. A execução dessa nova palavra pressiona o endereço na pilha para armazenar o valor da variável. Também existem constantes no forte, elas são criadas assim:
<> constant < >
Depois de criar a constante, a execução da palavra <nome da constante> coloca na pilha <valor>.
Portanto, tanto a variável de palavras quanto a constante de palavras são palavras geradoras. Eles são projetados para criar novas palavras. Em um forte, essas palavras são descritas usando a construção create ... does>.
Variáveis e constantes podem ser definidas da seguinte maneira:
: variable create 0 , does> ; : constant create , does> @ ;
O que tudo isso significa?
A palavra criar, quando executada, cria uma nova palavra com o nome que será usado quando executada a partir do fluxo de entrada. Após a criação, uma sequência de palavras é executada antes da palavra>. Mas no momento da execução desta palavra, o que é escrito depois do> é executado. Ao mesmo tempo, o endereço de dados já estará na pilha (como se costuma dizer no forte, “campos de dados”).
Assim, ao criar uma variável, a sequência “0” é executada - esta é a reserva de uma palavra de máquina com preenchimento zero. E quando a palavra criada é executada, nada é feito (depois de> não há nada). O endereço de memória onde o valor é armazenado simplesmente permanece na pilha.
Na definição de uma constante, uma palavra com um valor preenchido na pilha é reservada. Quando a palavra criada é executada, "@" é executado, que recupera o valor no endereço especificado.
Agora vamos pensar em como a palavra que criamos pode ser organizada. Ele envia o endereço de dados para a pilha (como var0) e transfere o controle para um endereço específico, bytecode. O comando var0 retorna imediatamente. Mas, neste caso, precisamos fazer não um retorno, mas, de fato, uma transição.
Mais uma vez vou formular o que precisa ser feito:
- colocar endereço de dados na pilha
- pule para um pedaço de código depois do>
Acontece que você só precisa transferir o controle para outro endereço de bytecode, mas primeiro coloque o endereço do próximo byte (R8) na pilha.
É quase um comando de ramificação! E aqui ela não está sozinha. Já tem branch8 e branch16. Vamos nomear os novos comandos var8 e var16 e deixar que esses sejam apenas os pontos de entrada para os comandos de ramificação. Economizamos na transição para a equipe de transição :) Então, será assim:
b_var8 = 0x29 bcmd_var8: push r8 b_branch8 = 0x10 bcmd_branch8: movsx rax, byte ptr [r8] add r8, rax jmp _next b_var16 = 0x30 bcmd_var16: push r8 b_branch16 = 0x11 bcmd_branch16: movsx rax, word ptr [r8] add r8, rax jmp _next
De uma maneira boa, o comando var32 ainda funcionará e o var64 também. Não temos transições tão longas, pois as transições comuns não são tão longas. Mas para o comando var, este é um caso muito realista. Mas, por enquanto, não faremos esses comandos. Faremos mais tarde, se necessário.
Com as palavras-geradores resolvidas. Foi a vez de decidir sobre o dicionário.
Vocabulário
Geralmente, quando eles falam de maneira simplista sobre o dicionário forte, ele é apresentado na forma de uma lista unidirecional de entradas do dicionário. De fato, tudo é um pouco mais complicado, pois o forte suporta muitos dicionários. De fato, eles são uma árvore. A busca por uma palavra nessa árvore começa com uma "folha" - esta é a última palavra no dicionário atual. O dicionário atual é definido pela variável de contexto e o endereço da última palavra está na palavra do dicionário. Outra variável é usada para gerenciar dicionários - ela define um dicionário onde novas palavras serão adicionadas. Assim, um dicionário pode ser instalado para uma pesquisa e outro para incluir novas palavras.
Para o nosso caso simples, seria possível não dar suporte a muitos dicionários, mas decidi não simplificar nada. De fato, para entender o código de bytes, a máquina de bytes, não é necessário saber o que está descrito nesta seção. Portanto, quem não está interessado, pode simplesmente pular esta seção. Bem, quem quer saber os detalhes - vá em frente!
Inicialmente, há um dicionário básico nomeado adiante. Isso significa que existe tal palavra - adiante. Esta palavra também é chamada de "dicionário", há alguma confusão. Portanto, quando se trata de uma palavra, chamarei de palavra do dicionário.
Novos dicionários são criados usando esta construção:
vocabulary < >
Isso cria uma palavra com o nome <nome do dicionário criado>. Quando executada, essa palavra definirá o dicionário criado como o dicionário inicial da pesquisa.
De fato, na palavra do dicionário, há um link para o último artigo deste dicionário, com o qual a pesquisa começa. E no momento da execução, essa palavra do dicionário grava um link para seu campo de dados na variável de contexto.
Mais tarde, será possível fazer o vocabulário da palavra, que no forte, na implementação atual, é descrito de maneira bastante simples:
: vocabulary create context @ , does> context ! ;
Então, crie a palavra adiante. Vamos usar o comando var8. Bytecode "contexto!" coloque logo após o campo de dados:
forth: .byte b_var8 .byte does_voc - . - 1 .quad 0 # <-- . , - . does_voc: .byte b_call8 .byte context - . - 1 .byte b_set .byte b_exit
Agora, de volta à criação do próprio dicionário.
Em geral, em um forte, a descrição de uma palavra na memória é chamada de "entrada de dicionário". Em termos comuns, eu diria que existe um título para o artigo e seu código. Mas nem tudo é muito usual em um forte: lá é chamado de "campo de nome", "campo de comunicação", "campo de código" e "campo de dados". Vou tentar lhe dizer o que tudo isso significa em termos tradicionais.
O campo de nome é o nome da palavra "linha com um contador". É como no pascal antigo - byte do comprimento da string e depois na string. O campo link é um link para o artigo anterior. Anteriormente, havia apenas um endereço, mas teremos um código independente de plataforma, e isso será um deslocamento. O campo de código, tradicionalmente no forte, é o código da máquina (quando a implementação está em uma linha direta), para palavras fora do kernel havia a chamada _call. Teremos apenas um bytecode. E o campo de dados é para palavras que contêm dados - por exemplo, para variáveis ou constantes. A propósito, o dicionário de palavras também se refere a ele.
Para o compilador, ainda precisamos de sinalizadores. Geralmente, um forte precisa de apenas uma bandeira - imediata, e é colocada em um byte longo (às vezes há outra - oculta). Mas isso é para código costurado direto, onde o controle do processador é transferido quando chamado para o campo de código. E temos palavras diferentes - bytecode e código de máquina, e pelo menos dois ou mesmo três sinalizadores são necessários.
Quanto é necessário para o campo da comunicação? No começo, eu queria usar 16 bits. Este é um link para a palavra anterior, e a palavra é definitivamente menor que 64 Kb. Mas então lembrei que a palavra pode conter dados de quase qualquer tamanho. Além disso, na presença de vários dicionários, o link pode passar por muitas palavras. Acontece que, na maioria dos casos, 8 bits são suficientes, mas podem haver 16 e 32. E até 64 bits, se houver dados com mais de 4 GB. Bem, vamos dar suporte a todas as opções. Qual opção é usada - coloque as bandeiras. Acontece pelo menos 4 sinalizadores: o atributo imediato, o atributo da palavra principal e 2 bits por variante do campo de comunicação usado. É necessário usar um byte separado para sinalizadores, de nenhuma outra maneira.
Definimos os sinalizadores da seguinte maneira:
f_code = 0x80 f_immediate = 0x60
O sinalizador f_code será para palavras do kernel escritas em assembler, o sinalizador f_immediate será útil para o compilador, sobre isso no próximo artigo. E os dois bits menos significativos determinarão o comprimento do campo de comunicação (1, 2, 4 ou 8 bytes).
Portanto, o título do artigo será assim:
- sinalizadores (1 byte)
- campo de comunicação (1-8 bytes)
- nome comprimento byte
- nome (1-255 bytes)
Até o momento, não usei os recursos do assembler "macro". E agora precisamos deles. É assim que eu tenho uma macro com o item de nome para formar o título da palavra:
.macro item name, flags = 0 link = . - p_item 9: .if link >= -256/2 && link < 256/2 .byte \flags .byte link .elseif link >= -256*256/2 && link < 256*256/2 .byte \flags | 1 .word . - p_item .elseif link >= -256*256*256*256/2 && link < 256*256*256*256/2 .byte \flags | 2 .int . - p_item .elseif link >= -256*256*256*256*256*256*256*256/2 && link < 256*256*256*256*256*256*256*256/2 .byte \flags | 3 .quad . - p_item .endif p_item = 9b .byte 9f - . - 1 .ascii "\name" 9: .endm
Essa macro usa o valor p_item - este é o endereço da entrada anterior do dicionário. Este valor no final é atualizado para uso futuro: p_item = 9b. Aqui 9b é um rótulo, não um número, não confunda :) A macro possui dois parâmetros - o nome da palavra e os sinalizadores (opcional). No início da macro, o deslocamento para a palavra anterior é calculado. Então, dependendo do tamanho do deslocamento, os sinalizadores e o campo de comunicação do tamanho desejado são compilados. Em seguida, o byte do comprimento do nome e o próprio nome.
Defina antes da primeira palavra p_item da seguinte maneira:
p_item = .
O ponto é o endereço de compilação atual no assembler. Como resultado dessa definição, a primeira palavra se referirá a si mesma (o campo de comunicação será 0). Este é um sinal do fim dos dicionários.
A propósito, o que haverá no campo de código das palavras do kernel? No mínimo, você deve salvar o código de comando em algum lugar. Eu decidi seguir o caminho mais simples. Para palavras do kernel, também haverá um bytecode. Para a maioria das equipes, este será apenas um comando de byte, seguido de b_exit. Portanto, para o intérprete, o sinalizador f_code não precisa ser analisado e os comandos para ele não serão diferentes de forma alguma. Você só precisa chamar o bytecode para todos.
Há outra vantagem nessa opção. Para comandos com parâmetros, você pode especificar parâmetros seguros. Por exemplo, se você chamar o comando aceso nas implementações do Fort com código de costura direta, o sistema falhará. E aqui está escrito lá, por exemplo, acende 0, e essa sequência simplesmente coloca 0 na pilha. Mesmo para o ramo pode ser feito com segurança!
.byte branch8 .byte 0f - . 0: .byte b_exit
Com essa ligação, haverá alguma sobrecarga, mas para o intérprete, elas não serão significativas. E o compilador analisará os sinalizadores e compilará o código correto e rápido.
A primeira palavra, é claro, será a palavra "adiante" - o vocabulário básico que estamos criando. Aqui, apenas venha com o prático comando var com um link para o código depois do>. Eu já citei esse código na seção anterior, mas vou repeti-lo novamente, com o título:
p_item = . item forth .byte b_var8 .byte does_voc - . - 1 .quad 0 does_voc: .byte b_call8 .byte context - . - 1 .byte b_set .byte b_exit
E imediatamente criaremos as variáveis de contexto e, precisamos delas para procurar palavras:
item .byte b_var0 .quad 0 item context context: .byte b_var0 .quad 0
E agora, você precisa ser paciente e escrever um título para cada palavra que escrevemos no assembler com o sinalizador f_code:
item 0, f_code .byte b_num0 .byte b_exit item 1, f_code .byte b_num1 .byte b_exit ... item 1-, f_code .byte b_wm .byte b_exit item 1+, f_code .byte b_wp .byte b_exit item +, f_code .byte b_add .byte b_exit item -, f_code .byte b_sub .byte b_exit item *, f_code .byte b_mul .byte b_exit
E assim por diante ...
Com equipes escritas em bytecode é ainda mais fácil. Basta adicionar apenas um cabeçalho antes do bytecode, assim como a palavra adiante, por exemplo:
item hold hold: .byte b_call8 .byte holdpoint - . - 1 # holdpoint ...
Para comandos com parâmetros, criaremos parâmetros seguros. Por exemplo, deixe que os comandos lite retornem o número Pi, se alguém os chamar de forma interativa, haverá uma páscoa :)
item lit8, f_code .byte b_lit8 .byte 31 .byte b_exit item lit16, f_code .byte b_lit16 .word 31415 .byte b_exit item lit32, f_code .byte b_lit32 .int 31415926 .byte b_exit item lit64, f_code .byte b_lit64 .quad 31415926535 .byte b_exit
A última palavra da lista fará com que a palavra se adeque simbolicamente. Mas ainda precisamos inicializar o endereço dessa palavra no campo de dados adiante. Para obter o endereço dessa palavra, use o comando var0:
last_item: .byte b_var0 item bye, f_code .byte b_bye
Nesse design, se chamarmos o endereço last_item no bytecode, obteremos o endereço da palavra bye. Para escrevê-lo nos campos de dados da palavra adiante, execute-o e o endereço desejado estará em contexto. Assim, o código de inicialização do sistema será assim:
forth last_item context @ !
E agora vamos prosseguir diretamente para o intérprete. Primeiro, precisamos trabalhar com o buffer de entrada e extrair palavras dele. Deixe-me lembrá-lo que o intérprete no forte é muito simples. Ele extrai as palavras do buffer de entrada em sequência, tenta encontrá-las. Se a palavra for encontrada, o intérprete a inicia para execução.
Buffer de entrada e extração de palavras
Para ser sincero, não quero gastar muito tempo estudando os padrões do forte. Mas ainda vou tentar torná-lo o mais próximo possível deles, principalmente de memória. Se os especialistas do forte perceberem uma forte discrepância aqui - escreva, eu corrigirei.
O forte possui três variáveis para trabalhar com o buffer: tib, #tib e> in. A variável tib envia o endereço do buffer de entrada na pilha. A variável #tib envia o número de caracteres que estão no buffer para a pilha. E a variável> in contém o deslocamento no buffer de entrada, além do qual o texto bruto está localizado. Defina essas variáveis.
item tib .byte b_var0 v_tib: .quad 0 item #tib .byte b_var0 v_ntib: .quad 0 item >in .byte b_var0 v_in: .quad 0
Em seguida, fazemos a palavra blword. Essa palavra, usando as variáveis especificadas, obtém a próxima palavra do fluxo de entrada. Um espaço é usado como delimitadores e todos os caracteres com um código menor que um espaço. Esta palavra estará em assembler. Após a depuração, ficou assim:
b_blword = 0xF0 bcmd_blword: mov rsi, v_tib # mov rdx, rsi # RDX mov rax, v_in # mov rcx, v_ntib # add rsi, rax # RSI - sub rcx, rax # jz 3f word2: lodsb # AL RSI cmp al, ' ' ja 1f # ( ) dec rcx jnz word2 # 3: sub rsi, rdx mov v_in, rsi push rcx jmp _next 1: lea rdi, [rsi - 1] # RDI = RSI - 1 ( ) dec rcx word3: lodsb cmp al, ' ' jbe 2f dec rcx jnz word3 2: mov rax, rsi sub rsi, rdx # ( ) mov v_in, rsi sub rax, rdi dec rax jz word1 push rdi # word1: push rax # jmp _next
Essa palavra é semelhante à palavra padrão, mas, diferentemente dela, leva em consideração todos os delimitadores e não copia a palavra no buffer. Ele retorna apenas dois valores na pilha - endereço e comprimento. Se a palavra não puder ser recuperada, retornará 0. Chegou a hora de começar a escrever o intérprete.
Pesquisa de palavras e intérprete
Para começar, vamos fazer a palavra interpretar. Essa palavra seleciona uma nova palavra do buffer usando blworld, a procura no dicionário e a executa. E assim se repete até que o buffer esteja esgotado. Ainda não temos a capacidade de procurar uma palavra, portanto, escreveremos um esboço de teste que simplesmente imprimirá a palavra no buffer usando o tipo Isso nos dará a oportunidade de verificar e depurar o blworld:
# : interpret begin blword dup while type repeat drop ; item interpret 1: .byte b_blword .byte b_dup .byte b_qnbranch8 .byte 0f - . .byte b_type .byte b_branch8 .byte 1b - . 0: .byte b_drop .byte b_exit
Agora faça a palavra sair. Geralmente eles fazem isso ao implementar sistemas fortes: eles usam a palavra quit ou abort para entrar no modo intérprete. A palavra sair libera pilhas e inicia um loop infinito de entrada e interpretação de buffer. Conosco, será apenas um chamado para interpretar. O código para esta palavra consistirá em duas partes. A primeira parte será em assembler, a segunda parte será em bytecode. A primeira parte:
b_quit = 0xF1 bcmd_quit: lea r8, quit mov sp, init_stack mov bp, init_rstack jmp _next
A segunda parte:
quit: .byte b_call16 .word interpret - . - 2 .byte b_bye
Como de costume, o código do assembler está localizado na seção .text, o código do byte está na seção .data.
E, finalmente, altere o bytecode inicial. Só haverá inicialização do dicionário, configurando um buffer na linha de partida e chamando quit.
# forth last_item context @ ! start_code tib ! < > #tib ! quit start: .byte b_call16 .word forth - . - 2 .byte b_call16 .word last_item - . - 2 .byte b_call16 .word context - . - 2 .byte b_get .byte b_set .byte b_call8 .byte start_code - . - 1 .byte b_call16 .word tib - . - 2 .byte b_set .byte b_lit16 .world 1f - 0f .byte b_call16 .word ntib - . - 2 .byte b_set .byte b_quit start_code: .byte b_var0 0: .ascii "word1 word2 word3" 1:
Compilar, vincular, executar!
$ as forth.s -o forth.o -g -ahlsm >list.txt $ ld forth.o -o forth $ ./forth word1word2wordBye!
É um pouco como mingau, mas é exatamente isso que deve ser o resultado. Nós produzimos sem delimitadores. By the way, coloque o feed de linha antes de comprar para o futuro, isso não vai doer.
Claro, eu tive que mexer na depuração. Além da já mencionada “falha de segmentação (núcleo despejado)”, algumas vezes foram obtidos resultados interessantes. Por exemplo, isto:
$ ./forth word1word2word3forth)%60Acurrent(context(%600lit8lit16zlit32v%5E%DF%80lit64v%5E%DF%80call8call16call32branch8branch16qbranch8qbranch16exit1-+!-%22*#/$mod%25/mod&abs'dup0drop1swap2rot3-rot4over5pick6roll7depth8@@!Ac@Bc!Cw@Dw!Ei@Fi!G0=P0%3CQ0%3ER=S%3CT%3EU%3C=V%3E=Wvar8)var160base(holdbuf(Qholdpoint(hold@0U110ACp@&20T0!?!%3CgF!A0@RF!5%220'%DE%A61Q-%DD%80:tib(%7F%60(%3Ein(%20%20%20%20%20%20%20interpret01('byeSegmentation%20fault%20(core%20dumped)
Este parece ser o nosso dicionário binário inteiro, com o texto cortado em delimitadores :) Aconteceu quando esqueci “dec rcx” antes da palavra3 no comando b_blword.
Podemos escolher palavras do fluxo de entrada, existe um dicionário. Agora você precisa implementar uma pesquisa no dicionário e iniciar palavras para execução. Isso exigirá as palavras find, cfa e execute.
A palavra localizar terá o endereço da palavra e seu comprimento da pilha. Esta palavra será retornada pelo endereço da entrada do dicionário ou 0 se não for encontrado.
A palavra cfa no endereço do artigo calculará o endereço do bytecode executável.
E a palavra executar executará o bytecode.
Vamos começar com o find. Nos padrões do forte, é preciso um endereço - uma linha com um contador. Mas eu não quero copiar mais uma vez a string para o buffer, então vou desviar um pouco dos padrões. A palavra find terá dois parâmetros na pilha - o endereço e o comprimento da string (de fato, que retorna a palavra blword). Após a depuração, essa palavra assumiu a seguinte forma:
b_find = 0xF2 bcmd_find: pop rbx # pop r9 # mov rdx, v_context mov rdx, [rdx] # # find0: mov al, [rdx] # and al, 3 # - , , or al, al jz find_l8 cmp al, 1 jz find_l16 cmp al, 2 jz find_l32 mov r10, [rdx + 1] # 64 lea rsi, [rdx + 9] # jmp find1 find_l32: movsx r10, dword ptr [rdx + 1] # 32 lea rsi, [rdx + 5] # jmp find1 find_l16: movsx r10, word ptr [rdx + 1] # 16 lea rsi, [rdx + 3] # jmp find1 find_l8: movsx r10, byte ptr [rdx + 1] # 8 lea rsi, [rdx + 2] # find1: movzx rax, byte ptr [rsi] # cmp rax, rbx jz find2 # find3: or r10, r10 jz find_notfound # , add rdx, r10 # jmp find0 # , find2: inc rsi mov rdi, r9 mov rcx, rax repz cmpsb jnz find3 # push rdx jmp _next find_notfound: push r10 jmp _next
Talvez esta seja a palavra mais difícil para hoje. Agora, modificamos a palavra interpretar, substituindo type por "find". # : interpret begin blword dup while find . repeat drop ; item interpret interpret: .byte b_blword .byte b_dup .byte b_qnbranch8 .byte 0f - . .byte b_find .byte b_call16 .word dot - . - 2 .byte b_branch8 .byte interpret - . 0: .byte b_drop .byte b_exit
Na linha de teste, você precisa colocar as palavras que estão no dicionário, por exemplo, "0 1- dup +.".Está tudo pronto para o lançamento! $ ld forth.o -o forth $ ./forth 6297733 6297898 6298375 Bye!
Ótimo, a pesquisa funciona. Estes são os endereços das palavras (em decimal). Agora a palavra cfa. Seja também em assembler, é muito simples, trabalhar com flags é semelhante a encontrar: b_cfa = 0xF3 bcmd_cfa: pop rdx # mov al, [rdx] # and al, 3 # - , , or al, al jz cfa_l8 cmp al, 1 jz cfa_l16 cmp al, 2 jz cfa_l32 lea rsi, [rdx + 9] # (64 ) jmp cfa1 find_l32: lea rsi, [rdx + 5] # (32 ) jmp cfa1 find_l16: lea rsi, [rdx + 3] # (16 ) jmp cfa1 find_l8: lea rsi, [rdx + 2] # (8 ) xor rax, rax lodsb add rsi, rax push rsi jmp _next
E, finalmente, a palavra executar, é ainda mais simples: b_execute = 0xF4 bcmd_execute: sub rbp, 8 mov [rbp], r8 # pop r8 # - jmp _next
Corrija a palavra interpretar e executar! # : interpret begin blword dup while find cfa execute repeat drop ; item interpret interpret: .byte b_blword .byte b_dup .byte b_qnbranch8 .byte 0f - . .byte b_find .byte b_cfa .byte b_execute .byte b_branch8 .byte interpret - . 0: .byte b_drop .byte b_exit
Lançamento: $ as forth.s -o forth.o -g -ahlsm >list.txt $ ld forth.o -o forth $ ./forth -2 Bye!
Urrra, ganhou! (C) Cat MatroskinDe fato, se você subtrair 1 de 0 e adicionar o resultado a si mesmo, será -2 :)Isso é ótimo, mas ainda quero digitar os comandos do teclado. E há mais um problema - nosso intérprete entende apenas os números 0, 1, 2, 3, 4 e 8 (que são definidos como constantes). O que ele aprenderia a entender qualquer número, você precisa da palavra "número?". Da mesma maneira que para a palavra find, não usarei o buffer. A palavra "número?" terá dois parâmetros na pilha - o endereço da string e o comprimento. Se for bem-sucedido, ele retornará o número recebido e o sinalizador 1. Se a conversão não for bem-sucedida, haverá um número na pilha: 0.O código acabou sendo longo, mas simples e linear: b_number = 0xF5 bcmd_number: pop rcx # pop rsi # xor rax, rax # xor rbx, rbx # mov r9, v_base # xor r10, r10 # or rcx, rcx jz num_false mov bl, [rsi] cmp bl, '+' jnz 1f inc rsi dec rcx jz num_false jmp num0 1: cmp bl, '-' jnz num0 mov r10, 1 inc rsi dec rcx jz num_false num0: mov bl, [rsi] cmp bl, '0' ja num_false cmp bl, '9' jae num_09 cmp bl, 'A' ja num_false cmp bl, 'Z' jae num_AZ cmp bl, 'a' ja num_false sub bl, 'a' - 10 jmp num_check num_AZ: sub bl, 'A' - 10 jmp num_check num_09: sub bl, '0' num_check: cmp rbx, r9 jge num_false add rax, rbx mul r9 inc rsi dec rcx jnz num0 or r10, r10 push rax push 1 jmp _next num_false: xor rcx, rcx push rcx jmp _next
Modifique interpretar. Se a palavra não estiver no dicionário, tentaremos interpretá-la como um número: # : interpret # begin # blword dup # while # over over find dup # if -rot drop drop cfa execute else number? drop then # repeat # drop ; item interpret interpret: .byte b_blword .byte b_dup .byte b_qnbranch8 .byte 0f - . .byte b_over .byte b_over .byte b_find .byte b_dup .byte b_qnbranch8 .byte 1f - . .byte b_mrot .byte b_drop .byte b_drop .byte b_cfa .byte b_execute .byte b_branch8 .byte 2f - . 1: .byte b_numberq .byte b_drop 2: .byte b_branch8 .byte interpret - . 0: .byte b_drop .byte b_exit last_item: .byte b_var0 item bye, f_code .byte b_bye
E aqui cheguei! Depure esse bytecode no assembler, sem pontos de interrupção no bytecode, sem a capacidade de apenas "avançar" ao longo do bytecode ... Além disso, sem os movimentos mais fáceis da pilha e sem a simples capacidade de visualizar o conteúdo da pilha ... E no GDB, onde apenas a linha de comando ... Eu vou te dizer - é apenas uma explosão cerebral! Não é pior. Esta é uma explosão cerebral !Mas ... somos índios, sempre encontraremos soluções alternativas :)Em geral, encontrei esta solução: implementei um comando para exibir o conteúdo da pilha - "s". O comando não é o mais fácil, mas ainda mais fácil de interpretar. E, como se viu, ochchchen útil. Aqui está: # : .s depth dup . c": emit do dup while dup pick . 1- again drop ; item .s # 11 22 33 prstack: .byte b_depth # 11 22 33 3 .byte b_dup # 11 22 33 3 3 .byte b_lit8 .byte '(' .byte b_emit .byte b_call16 # 11 22 33 3 .word dot - . - 2 .byte b_strp # 11 22 33 3 .byte 3 .ascii "): " 1: .byte b_dup # 11 22 33 3 3 .byte b_qnbranch8 # 11 22 33 3 .byte 2f - . .byte b_dup # 11 22 33 3 3 .byte b_pick # 11 22 33 3 11 .byte b_call16 # 11 22 33 3 .word dot - . - 2 .byte b_wm # 11 22 33 2 .byte b_branch8 .byte 1b - . 2: .byte b_drop # 11 22 33 .byte b_exit
À direita, dei um exemplo do conteúdo da pilha, após a execução de cada comando. Claro, há um ciclo, e esta é apenas a primeira passagem. Mas o resto é muito parecido, apenas o valor no topo da pilha muda. Após esse "rastreio", a equipe ganhou imediatamente!Para depuração, criei as seguintes macros: .macro prs new_line = 1 .byte b_call16 .word prstack - . - 2 .if \new_line > 0 .byte b_lit8, '\n' .byte b_emit .endif .endm
Usado inserindo nos lugares certos desta maneira: item interpret interpret: .byte b_blword prs .byte b_dup prs .byte b_qnbranch8 .byte 0f - . .byte b_over .byte b_over ......
Como resultado, o primeiro lançamento produziu a seguinte saída: $ ./forth (2 ): 6297664 1 (3 ): 6297664 1 1 (3 ): 2 6297666 1 (4 ): 2 6297666 1 1 (4 ): 2 3 6297668 1 (5 ): 2 3 6297668 1 1 (3 ): 6 6297670 2 (4 ): 6 6297670 2 2 (4 ): 6 6297670 6297673 1 (5 ): 6 6297670 6297673 1 1 6297670 (2 ): 6 0 (3 ): 6 0 0 Bye!
Cada movimento na pilha pode ser visto claramente. Era necessário fazer isso mais cedo :)Fui mais longe, fazendo outra macro de depuração: .macro pr string .byte b_strp .byte 9f - 8f 8: .ascii "\n\string" 9: .endm
Como resultado, tornou-se possível: item interpret interpret: .byte b_blword pr blworld prs .byte b_dup .byte b_qnbranch8 .byte 0f - . .byte b_over .byte b_over prs .byte b_find pr find prs .byte b_dup .byte b_qnbranch8 .byte 1f - . .byte b_mrot .byte b_drop .byte b_drop .byte b_cfa pr execute prs .byte b_execute .byte b_branch8 .byte 2f - . 1: .byte b_numberq pr numberq prs .byte b_drop 2: .byte b_branch8 .byte interpret - . 0: .byte b_drop .byte b_exit
E pegue isso: $ ./forth blworld(2 ): 6297664 2 (4 ): 6297664 2 6297664 2 find(3 ): 6297664 2 0 numberq(2 ): 6297664 0 blworld(3 ): 6297664 6297667 2 (5 ): 6297664 6297667 2 6297667 2 find(4 ): 6297664 6297667 2 0 numberq(3 ): 6297664 6297667 0 blworld(4 ): 6297664 6297667 6297670 1 (6 ): 6297664 6297667 6297670 1 6297670 1 find(5 ): 6297664 6297667 6297670 1 6297958 execute(3 ): 6297664 6297667 6297962 blworld(3 ): 39660590749888 6297672 1 (5 ): 39660590749888 6297672 1 6297672 1 find(4 ): 39660590749888 6297672 1 6298496 execute(2 ): 39660590749888 6298500 39660590749888 blworld(1 ): 0 Bye!
Foi uma tentativa de interpretar a sequência "20 30 *.".E você pode exibir os números das linhas de origem ... ok, talvez então ...É claro que essa é uma técnica clássica de registro para depuração, mas algo que não me lembrei imediatamente.Em geral, como resultado da depuração, encontrei uma pilha no exterior. É o oposto do estouro quando eles tentam levar mais do que colocam. Adicionado controle dela para ".s".Com a ajuda de novas macros, a depuração foi rápida. A propósito, antes disso eu postei um bytecode por linha. Mas o assembler permite que você coloque vários bytes em uma string, por que não usá-lo.Vamos concluir a palavra interpretar adicionando duas verificações: que a palavra não foi convertida em um número e sair da pilha no exterior. Como resultado, interpretar é o seguinte: item interpret interpret: .byte b_blword .byte b_dup .byte b_qnbranch8 .byte 0f - . .byte b_over .byte b_over .byte b_find .byte b_dup .byte b_qnbranch8 .byte 1f - . .byte b_mrot .byte b_drop .byte b_drop .byte b_cfa .byte b_execute .byte b_branch8 .byte 2f - . 1: .byte b_drop .byte b_over, b_over .byte b_numberq # , .byte b_qbranch8, 3f - . # 0, , 3 .byte b_type # .byte b_strp # .byte 19 # .ascii " : word not found!\n" .byte b_quit # 3: .byte b_nip, b_nip # , ( b_over, b_over) 2: # .byte b_depth # .byte b_zlt # , 0 ( 0<) .byte b_qnbranch8, interpret_ok - . # , , .byte b_strp # .byte 14 .ascii "\nstack fault!\n" .byte b_quit # interpret_ok: .byte b_branch8 .byte interpret - . 0: .byte b_drop .byte b_exit
A propósito, vale a pena notar que agora o comando quit libera pilhas e inicia a interpretação novamente sem alterar o estado do buffer. Assim, a interpretação continua, mas com pilhas "novas". Vamos corrigir isso um pouco mais tarde.A única coisa que resta é organizar a entrada do teclado.Entrada do teclado
A entrada do teclado no forte é simples. Existe a palavra expect, são necessários dois parâmetros - o endereço do buffer e seu tamanho. Esta palavra executa a entrada do teclado. O número real de caracteres digitados é colocado na variável span. Vamos fazer essas palavras. Entraremos a partir da entrada padrão. .data item span span: .byte b_var0 v_span: .quad 0 .text b_expect = 0x88 bcmd_expect: mov rax, 0 # № 1 - sys_read mov rdi, 0 # № 1 - stdout pop rdx # pop rsi # push r8 syscall # pop r8 mov rbx, rax or rax, rax jge 1f xor rbx, rbx 1: mov v_span, rbx jmp _next
Agora precisamos criar um buffer de entrada do teclado. Que seja 256 caracteres.Vamos substituí-lo na linha de teste anterior. inbuf_size = 256 inbuf: .byte b_var0 .space inbuf_size
E nós modificamos quit, assim como o código de início. Defina a variável tib como o buffer de entrada inbuf, chame expect e copie o valor de span para #tib. A variável> in é anulada; chamamos interpretar. E assim repetimos em um ciclo. Existem bugigangas - para adicionar um prompt de entrada e seria bom exibir o status da pilha (e já temos um comando pronto para isso!). Após várias iterações, obtivemos o seguinte código (comando start e quit): # forth last_item context @ ! quit start: .byte b_call16 .word forth - . - 2 .byte b_call16 .word last_item - . - 2 .byte b_call16 .word context - . - 2 .byte b_get .byte b_set .byte b_quit inbuf: .byte b_var0 .space inbuf_size # begin inbuf dup tib ! inbuf_size expect span @ #tib ! 0 >in ! interpret again quit: .byte b_strp, 1 .ascii "\n" .byte b_call16 .word prstack - . - 2 .byte b_strp .byte 2 .ascii "> " .byte b_call16 .word inbuf - . - 2 .byte b_dup .byte b_call16 .word tib - . - 2 .byte b_set .byte b_lit16 .word inbuf_size .byte b_expect .byte b_call16 .word span - . - 2 .byte b_get .byte b_call16 .word ntib - . - 2 .byte b_set .byte b_num0 .byte b_call16 .word bin - . - 2 .byte b_set .byte b_call16 .word interpret - . - 2 .byte b_branch8, quit - .
E aqui está o resultado: $ ./forth ( 0 ): > 60 ( 1 ): 60 > 60 24 ( 3 ): 60 60 24 > rot ( 3 ): 60 24 60 > -rot ( 3 ): 60 60 24 > swap ( 3 ): 60 24 60 > * * . 86400 ( 0 ): > 200 30 /mod ( 2 ): 20 6 > bye Bye! $
Tudo depois do símbolo ">" é minha entrada de teclado. O resto é a resposta do sistema. Eu brinquei um pouco com os comandos, digitando no teclado. Ele executou várias operações de pilha, calculou o número de segundos em dias.Sumário
O intérprete está completo e funcionando. E educadamente diz adeus - para ele “tchau” e ele “tchau” :)Como convite - o conteúdo da pilha aritmética. O primeiro número entre colchetes é o tamanho da pilha, o conteúdo e o prompt para inserir ">". Você pode inserir qualquer comando implementado (contei 76 comandos). É verdade que muitos fazem sentido apenas para o compilador - por exemplo, literais, transições, comandos de chamada.Fonte completa (cerca de 1300 linhas) .intel_syntax noprefix stack_size = 1024 f_code = 0x80 f_immediate = 0x60 .macro item name, flags = 0 link = p_item - . 9: .if link >= -256/2 && link < 256/2 .byte \flags .byte link .elseif link >= -256*256/2 && link < 256*256/2 .byte \flags | 1 .word link .elseif link >= -256*256*256*256/2 && link < 256*256*256*256/2 .byte \flags | 2 .int link .elseif link >= -256*256*256*256*256*256*256*256/2 && link < 256*256*256*256*256*256*256*256/2 .byte \flags | 3 .quad link .endif p_item = 9b .byte 9f - . - 1 .ascii "\name" 9: .endm .section .data init_stack: .quad 0 init_rstack: .quad 0 emit_buf: .byte 0 inbuf_size = 256 msg_bad_byte: .ascii "Bad byte code!\n" msg_bad_byte_len = . - msg_bad_byte # len msg_bye: .ascii "\nBye!\n" msg_bye_len = . - msg_bye bcmd: .quad bcmd_bad, bcmd_bye, bcmd_num0, bcmd_num1, bcmd_num2, bcmd_num3, bcmd_num4, bcmd_num8 # 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_qnbranch8, bcmd_qnbranch16,bcmd_bad, bcmd_exit # 0x10 .quad bcmd_wp, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_wm, bcmd_add, bcmd_sub, bcmd_mul, bcmd_div, bcmd_mod, bcmd_divmod, bcmd_abs # 0x20 .quad bcmd_var0, bcmd_var8, bcmd_var16, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_dup, bcmd_drop, bcmd_swap, bcmd_rot, bcmd_mrot, bcmd_over, bcmd_pick, bcmd_roll # 0x30 .quad bcmd_depth, bcmd_nip, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_get, bcmd_set, bcmd_get8, bcmd_set8, bcmd_get16, bcmd_set16, bcmd_get32, bcmd_set32 # 0x40 .quad bcmd_setp, bcmd_setm, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_zeq, bcmd_zlt, bcmd_zgt, bcmd_eq, bcmd_lt, bcmd_gt, bcmd_lteq, bcmd_gteq # 0x50 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad .quad bcmd_2r, bcmd_r2, bcmd_rget, 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_emit, bcmd_str, bcmd_strp, bcmd_count, bcmd_bad, bcmd_bad, bcmd_bad # 0x80 .quad bcmd_expect, 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 # 0x90 .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_blword, bcmd_quit, bcmd_find, bcmd_cfa, bcmd_execute, bcmd_numberq, bcmd_bad, bcmd_bad # 0xF0 .quad bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad, bcmd_bad # forth last_item context @ ! quit start: .byte b_call16 .word forth - . - 2 .byte b_call16 .word last_item - . - 2 .byte b_call16 .word context - . - 2 .byte b_get .byte b_set .byte b_quit inbuf: .byte b_var0 .space inbuf_size # begin inbuf dup tib ! inbuf_size expect span @ #tib ! 0 >in ! interpret again quit: .byte b_strp, 1 .ascii "\n" .byte b_call16 .word prstack - . - 2 .byte b_strp .byte 2 .ascii "> " .byte b_call16 .word inbuf - . - 2 .byte b_dup .byte b_call16 .word tib - . - 2 .byte b_set .byte b_lit16 .word inbuf_size .byte b_expect .byte b_call16 .word span - . - 2 .byte b_get .byte b_call16 .word ntib - . - 2 .byte b_set .byte b_num0 .byte b_call16 .word bin - . - 2 .byte b_set .byte b_call16 .word interpret - . - 2 .byte b_branch8, quit - . p_item = . item forth forth: .byte b_var8 .byte does_voc - . .quad 0 does_voc: .byte b_call8 .byte context - . - 1 .byte b_set .byte b_exit item current .byte b_var0 .quad 0 item context context: .byte b_var0 v_context: .quad 0 item 0, f_code .byte b_num0 .byte b_exit item 1, f_code .byte b_num1 .byte b_exit item 2, f_code .byte b_num2 .byte b_exit item 3, f_code .byte b_num3 .byte b_exit item 4, f_code .byte b_num4 .byte b_exit item 8, f_code .byte b_num8 .byte b_exit item lit8, f_code .byte b_lit8 .byte 31 .byte b_exit item lit16, f_code .byte b_lit16 .word 31415 .byte b_exit item lit32, f_code .byte b_lit32 .int 31415926 .byte b_exit item lit64, f_code .byte b_lit64 .quad 31415926 .byte b_exit item call8, f_code .byte b_call8 .byte 0f - . - 1 0: .byte b_exit item call16, f_code .byte b_call16 .word 0f - . - 2 0: .byte b_exit item call32, f_code .byte b_call32 .int 0f - . - 4 0: .byte b_exit item branch8, f_code .byte b_branch8 .byte 0f - . 0: .byte b_exit item branch16, f_code .byte b_branch16 .word 0f - . 0: .byte b_exit item qbranch8, f_code .byte b_qbranch8 .byte 0f - . 0: .byte b_exit item qbranch16, f_code .byte b_qbranch16 .word 0f - . 0: .byte b_exit item exit, f_code .byte b_exit item 1-, f_code .byte b_wm .byte b_exit item 1+, f_code .byte b_wp .byte b_exit item +, f_code .byte b_add .byte b_exit item -, f_code .byte b_sub .byte b_exit item *, f_code .byte b_mul .byte b_exit item /, f_code .byte b_div .byte b_exit item mod, f_code .byte b_mod .byte b_exit item /mod, f_code .byte b_divmod .byte b_exit item abs, f_code .byte b_abs .byte b_exit item dup, f_code .byte b_dup .byte b_exit item drop, f_code .byte b_drop .byte b_exit item swap, f_code .byte b_swap .byte b_exit item rot, f_code .byte b_rot .byte b_exit item -rot, f_code .byte b_mrot .byte b_exit item over, f_code .byte b_over .byte b_exit item pick, f_code .byte b_pick .byte b_exit item roll, f_code .byte b_roll .byte b_exit item depth, f_code .byte b_depth .byte b_exit item @, f_code .byte b_get .byte b_exit item !, f_code .byte b_set .byte b_exit item c@, f_code .byte b_get8 .byte b_exit item c!, f_code .byte b_set8 .byte b_exit item w@, f_code .byte b_get16 .byte b_exit item w!, f_code .byte b_set16 .byte b_exit item i@, f_code .byte b_get32 .byte b_exit item i!, f_code .byte b_set32 .byte b_exit item +!, f_code .byte b_setp .byte b_exit item -!, f_code .byte b_setm .byte b_exit item >r, f_code .byte b_2r .byte b_exit item r>, f_code .byte b_r2 .byte b_exit item r@, f_code .byte b_rget .byte b_exit item "0=", f_code .byte b_zeq .byte b_exit item 0<, f_code .byte b_zlt .byte b_exit item 0>, f_code .byte b_zgt .byte b_exit item "=", f_code .byte b_eq .byte b_exit item <, f_code .byte b_lt .byte b_exit item >, f_code .byte b_gt .byte b_exit item "<=", f_code .byte b_lteq .byte b_exit item ">=", f_code .byte b_gteq .byte b_exit item type, f_code .byte b_type .byte b_exit item expect, f_code .byte b_expect .byte b_exit item emit, f_code .byte b_emit .byte b_exit item count, f_code .byte b_count .byte b_exit item "(\")", f_code .byte b_str .byte b_exit item "(.\")", f_code .byte b_strp .byte b_exit item var8, f_code .byte b_var8 .byte 0f - . 0: .byte b_exit item var16, f_code .byte b_var16 .word 0f - . 0: .byte b_exit item base base: .byte b_var0 v_base: .quad 10 holdbuf_len = 70 item holdbuf holdbuf: .byte b_var0 .space holdbuf_len item holdpoint holdpoint: .byte b_var0 .quad 0 item span span: .byte b_var0 v_span: .quad 0 # : hold holdpoint @ 1- dup holdbuf > if drop drop else dup holdpoint ! c! then ; item hold hold: .byte b_call8 .byte holdpoint - . - 1 # holdpoint .byte b_get # @ .byte b_wm # 1- .byte b_dup # dup .byte b_call8 .byte holdbuf - . - 1 # holdbuf .byte b_gt # > .byte b_qbranch8 # if .byte 0f - . .byte b_drop # drop .byte b_drop # drop .byte b_branch8 # ( then) .byte 1f - . 0: .byte b_dup # dup .byte b_call8 .byte holdpoint - . - 1 # holdpoint .byte b_set # ! .byte b_set8 # c! 1: .byte b_exit # ; # : # base /mod swap dup 10 < if c" 0 + else 10 - c" A + then hold ; item # conv: .byte b_call16 .word base - . - 2 # base .byte b_get # @ .byte b_divmod # /mod .byte b_swap # swap .byte b_dup # dup .byte b_lit8 .byte 10 # 10 .byte b_lt # < .byte b_qnbranch8 # if .byte 0f - . .byte b_lit8 .byte '0' # c" 0 .byte b_add # + .byte b_branch8 # else .byte 1f - . 0: .byte b_lit8 .byte '?' # c" A .byte b_add # + 1: .byte b_call16 .word hold - . - 2 # hold .byte b_exit # ; # : <# holdbuf 70 + holdpoint ! ; item <# conv_start: .byte b_call16 .word holdbuf - . - 2 .byte b_lit8 .byte holdbuf_len .byte b_add .byte b_call16 .word holdpoint - . - 2 .byte b_set .byte b_exit # : #s do # dup 0=until ; item #s conv_s: .byte b_call8 .byte conv - . - 1 .byte b_dup .byte b_qbranch8 .byte conv_s - . .byte b_exit # : #> holdpoint @ holdbuf 70 + over - ; item #> conv_end: .byte b_call16 .word holdpoint - . - 2 .byte b_get .byte b_call16 .word holdbuf - . - 2 .byte b_lit8 .byte holdbuf_len .byte b_add .byte b_over .byte b_sub .byte b_exit item . dot: .byte b_dup .byte b_abs .byte b_call8 .byte conv_start - . - 1 .byte b_lit8 .byte ' ' .byte b_call16 .word hold - . - 2 .byte b_call8 .byte conv_s - . - 1 .byte b_drop .byte b_zlt .byte b_qnbranch8 .byte 1f - . .byte b_lit8 .byte '-' .byte b_call16 .word hold - . - 2 1: .byte b_call8 .byte conv_end - . - 1 .byte b_type .byte b_exit item tib tib: .byte b_var0 v_tib: .quad 0 item #tib ntib: .byte b_var0 v_ntib: .quad 0 item >in bin: .byte b_var0 v_in: .quad 0 # : .s depth dup . c": emit do dup while dup pick . 1- again drop ; item .s # 11 22 33 prstack: .byte b_depth # 11 22 33 3 .byte b_dup # 11 22 33 3 3 .byte b_strp .byte 2 .ascii "( " .byte b_call16 # 11 22 33 3 .word dot - . - 2 .byte b_strp # 11 22 33 3 .byte 3 .ascii "): " .byte b_dup, b_zlt .byte b_qnbranch8, 1f - . .byte b_strp .byte 14 .ascii "\nStack fault!\n" .byte b_quit 1: .byte b_dup # 11 22 33 3 3 .byte b_qnbranch8 # 11 22 33 3 .byte 2f - . .byte b_dup # 11 22 33 3 3 .byte b_pick # 11 22 33 3 11 .byte b_call16 # 11 22 33 3 .word dot - . - 2 .byte b_wm # 11 22 33 2 .byte b_branch8 .byte 1b - . 2: .byte b_drop # 11 22 33 .byte b_exit .macro prs new_line = 1 .byte b_call16 .word prstack - . - 2 .if \new_line > 0 .byte b_lit8, '\n' .byte b_emit .endif .endm .macro pr string .byte b_strp .byte 9f - 8f 8: .ascii "\n\string" 9: .endm item interpret interpret: .byte b_blword .byte b_dup .byte b_qnbranch8 .byte 0f - . .byte b_over .byte b_over .byte b_find .byte b_dup .byte b_qnbranch8 .byte 1f - . .byte b_mrot .byte b_drop .byte b_drop .byte b_cfa .byte b_execute .byte b_branch8 .byte 2f - . 1: .byte b_drop .byte b_over, b_over .byte b_numberq # , .byte b_qbranch8, 3f - . # 0, , 3 .byte b_type # .byte b_strp # .byte 19 # .ascii " : word not found!\n" .byte b_quit # 3: .byte b_nip, b_nip # , ( b_over, b_over) 2: # .byte b_depth # .byte b_zlt # , 0 ( 0<) .byte b_qnbranch8, interpret_ok - . # , , .byte b_strp # .byte 14 .ascii "\nstack fault!\n" .byte b_quit # interpret_ok: .byte b_branch8 .byte interpret - . 0: .byte b_drop .byte b_exit last_item: .byte b_var0 item bye, f_code .byte b_bye .section .text .global _start # _start: mov rbp, rsp sub rbp, stack_size lea r8, start mov init_stack, rsp mov init_rstack, rbp jmp _next b_var0 = 0x28 bcmd_var0: push r8 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_num1 = 0x03 bcmd_num1: push 1 jmp _next b_num2 = 0x04 bcmd_num2: push 2 jmp _next b_num3 = 0x05 bcmd_num3: push 3 jmp _next b_num4 = 0x06 bcmd_num4: push 4 jmp _next b_num8 = 0x07 bcmd_num8: push 8 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 = 0x30 bcmd_dup: push [rsp] jmp _next b_wm = 0x20 bcmd_wm: decq [rsp] jmp _next b_wp = 0x18 bcmd_wp: incq [rsp] jmp _next b_add = 0x21 bcmd_add: pop rax add [rsp], rax jmp _next b_sub = 0x22 bcmd_sub: pop rax sub [rsp], rax jmp _next b_mul = 0x23 bcmd_mul: pop rax pop rbx imul rbx push rax jmp _next b_div = 0x24 bcmd_div: pop rbx pop rax cqo idiv rbx push rax jmp _next b_mod = 0x25 bcmd_mod: pop rbx pop rax cqo idiv rbx push rdx jmp _next b_divmod = 0x26 bcmd_divmod: pop rbx pop rax cqo idiv rbx push rdx push rax jmp _next b_abs = 0x27 bcmd_abs: mov rax, [rsp] or rax, rax jge _next neg rax mov [rsp], rax jmp _next b_drop = 0x31 bcmd_drop: add rsp, 8 jmp _next b_swap = 0x32 bcmd_swap: pop rax pop rbx push rax push rbx jmp _next b_rot = 0x33 bcmd_rot: pop rax pop rbx pop rcx push rbx push rax push rcx jmp _next b_mrot = 0x34 bcmd_mrot: pop rcx pop rbx pop rax push rcx push rax push rbx jmp _next b_over = 0x35 bcmd_over: push [rsp + 8] jmp _next b_pick = 0x36 bcmd_pick: pop rcx push [rsp + 8*rcx] jmp _next b_roll = 0x37 bcmd_roll: pop rcx mov rbx, [rsp + 8*rcx] roll1: mov rax, [rsp + 8*rcx - 8] mov [rsp + 8*rcx], rax dec rcx jnz roll1 push rbx jmp _next b_depth = 0x38 bcmd_depth: mov rax, init_stack sub rax, rsp sar rax, 3 push rax jmp _next b_nip = 0x39 bcmd_nip: pop rax mov [rsp], rax jmp _next b_get = 0x40 bcmd_get: pop rcx push [rcx] jmp _next b_set = 0x41 bcmd_set: pop rcx pop rax mov [rcx], rax jmp _next b_get8 = 0x42 bcmd_get8: pop rcx movsx rax, byte ptr [rcx] push rax jmp _next b_set8 = 0x43 bcmd_set8: pop rcx pop rax mov [rcx], al jmp _next b_get16 = 0x44 bcmd_get16: pop rcx movsx rax, word ptr [rcx] push rax jmp _next b_set16 = 0x45 bcmd_set16: pop rcx pop rax mov [rcx], ax jmp _next b_get32 = 0x46 bcmd_get32: pop rcx movsx rax, dword ptr [rcx] push rax jmp _next b_set32 = 0x47 bcmd_set32: pop rcx pop rax mov [rcx], eax jmp _next b_setp = 0x48 bcmd_setp: pop rcx pop rax add [rcx], rax jmp _next b_setm = 0x49 bcmd_setm: pop rcx pop rax sub [rcx], rax jmp _next b_2r = 0x60 bcmd_2r: pop rax sub rbp, 8 mov [rbp], rax jmp _next b_r2 = 0x61 bcmd_r2: push [rbp] add rbp, 8 jmp _next b_rget = 0x62 bcmd_rget: push [rbp] jmp _next # 0= b_zeq = 0x50 bcmd_zeq: pop rax or rax, rax jnz rfalse rtrue: push -1 jmp _next rfalse: push 0 jmp _next # 0< b_zlt = 0x51 bcmd_zlt: pop rax or rax, rax jl rtrue push 0 jmp _next # 0> b_zgt = 0x52 bcmd_zgt: pop rax or rax, rax jg rtrue push 0 jmp _next # = b_eq = 0x53 bcmd_eq: pop rbx pop rax cmp rax, rbx jz rtrue push 0 jmp _next # < b_lt = 0x54 bcmd_lt: pop rbx pop rax cmp rax, rbx jl rtrue push 0 jmp _next # > b_gt = 0x55 bcmd_gt: pop rbx pop rax cmp rax, rbx jg rtrue push 0 jmp _next # <= b_lteq = 0x56 bcmd_lteq: pop rbx pop rax cmp rax, rbx jle rtrue push 0 jmp _next # >= b_gteq = 0x57 bcmd_gteq: pop rbx pop rax cmp rax, rbx jge rtrue push 0 jmp _next b_var8 = 0x29 bcmd_var8: push r8 b_branch8 = 0x10 bcmd_branch8: movsx rax, byte ptr [r8] add r8, rax jmp _next b_var16 = 0x30 bcmd_var16: push r8 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_qnbranch8 = 0x14 bcmd_qnbranch8: pop rax or rax, rax jz bcmd_branch8 inc r8 jmp _next b_qnbranch16 = 0x15 bcmd_qnbranch16:pop rax or rax, rax jz bcmd_branch16 add r8, 2 jmp _next b_bad = 0x00 bcmd_bad: mov rax, 1 # 1 - sys_write mov rdi, 1 # 1 stdout mov rsi, offset msg_bad_byte # mov rdx, msg_bad_byte_len # syscall # mov rax, 60 # 1 - sys_exit mov rbx, 1 # 1 syscall # b_bye = 0x01 bcmd_bye: mov rax, 1 # 1 - sys_write mov rdi, 1 # 1 stdout mov rsi, offset msg_bye # mov rdx, msg_bye_len # syscall # mov rax, 60 # 60 - sys_exit mov rdi, 0 # 0 syscall # b_strp = 0x83 bcmd_strp: movsx rax, byte ptr [r8] inc r8 push r8 add r8, rax push rax b_type = 0x80 bcmd_type: mov rax, 1 # 1 - sys_write mov rdi, 1 # 1 - stdout pop rdx # pop rsi # push r8 syscall # pop r8 jmp _next b_expect = 0x88 bcmd_expect: mov rax, 0 # 1 - sys_read mov rdi, 0 # 1 - stdout pop rdx # pop rsi # push r8 syscall # pop r8 mov rbx, rax or rax, rax jge 1f xor rbx, rbx 1: mov v_span, rbx jmp _next b_str = 0x82 bcmd_str: movzx rax, byte ptr [r8] lea r8, [r8 + rax + 1] jmp _next b_count = 0x84 bcmd_count: pop rcx movzx rax, byte ptr [rcx] inc rcx push rcx push rax jmp _next b_emit = 0x81 bcmd_emit: pop rax mov rsi, offset emit_buf # mov [rsi], al mov rax, 1 # 1 - sys_write mov rdi, 1 # 1 - stdout mov rdx, 1 # push r8 syscall # pop r8 jmp _next b_blword = 0xF0 bcmd_blword: mov rsi, v_tib # mov rdx, rsi # RDX mov rax, v_in # mov rcx, v_ntib # mov rbx, rcx add rsi, rax # RSI - sub rcx, rax # jz 3f word2: lodsb # AL RSI cmp al, ' ' ja 1f # ( ) dec rcx jnz word2 # 3: sub rsi, rdx mov v_in, rsi push rcx jmp _next 1: lea rdi, [rsi - 1] # RDI = RSI - 1 ( ) dec rcx jz word9 word3: lodsb cmp al, ' ' jbe 2f dec rcx jnz word3 word9: inc rsi 2: mov rax, rsi sub rsi, rdx # ( ) cmp rsi, rbx jle 4f mov rsi, rbx 4: mov v_in, rsi sub rax, rdi dec rax jz word1 push rdi # word1: push rax # jmp _next b_quit = 0xF1 bcmd_quit: lea r8, quit mov rsp, init_stack mov rbp, init_rstack jmp _next b_find = 0xF2 bcmd_find: pop rbx # pop r9 # mov rdx, v_context mov rdx, [rdx] # # find0: mov al, [rdx] # and al, 3 # - , , or al, al jz find_l8 cmp al, 1 jz find_l16 cmp al, 2 jz find_l32 mov r10, [rdx + 1] # 64 lea rsi, [rdx + 9] # jmp find1 find_l32: movsx r10, dword ptr [rdx + 1] # 32 lea rsi, [rdx + 5] # jmp find1 find_l16: movsx r10, word ptr [rdx + 1] # 16 lea rsi, [rdx + 3] # jmp find1 find_l8: movsx r10, byte ptr [rdx + 1] # 8 lea rsi, [rdx + 2] # find1: movzx rax, byte ptr [rsi] # cmp rax, rbx jz find2 # find3: or r10, r10 jz find_notfound # , add rdx, r10 # jmp find0 # , find2: inc rsi mov rdi, r9 mov rcx, rax repz cmpsb jnz find3 # push rdx jmp _next find_notfound: push r10 jmp _next b_cfa = 0xF3 bcmd_cfa: pop rdx # mov al, [rdx] # and al, 3 # - , , or al, al jz cfa_l8 cmp al, 1 jz cfa_l16 cmp al, 2 jz cfa_l32 lea rsi, [rdx + 9] # (64 ) jmp cfa1 cfa_l32: lea rsi, [rdx + 5] # (32 ) jmp cfa1 cfa_l16: lea rsi, [rdx + 3] # (16 ) jmp cfa1 cfa_l8: lea rsi, [rdx + 2] # (8 ) cfa1: xor rax, rax lodsb add rsi, rax push rsi jmp _next b_execute = 0xF4 bcmd_execute: sub rbp, 8 mov [rbp], r8 # pop r8 # - jmp _next b_numberq = 0xF5 bcmd_numberq: pop rcx # pop rsi # xor rax, rax # xor rbx, rbx # mov r9, v_base # xor r10, r10 # or rcx, rcx jz num_false mov bl, [rsi] cmp bl, '+' jnz 1f inc rsi dec rcx jz num_false jmp num0 1: cmp bl, '-' jnz num0 mov r10, 1 inc rsi dec rcx jz num_false num0: mov bl, [rsi] cmp bl, '0' jb num_false cmp bl, '9' jbe num_09 cmp bl, 'A' jb num_false cmp bl, 'Z' jbe num_AZ cmp bl, 'a' jb num_false cmp bl, 'z' ja num_false sub bl, 'a' - 10 jmp num_check num_AZ: sub bl, 'A' - 10 jmp num_check num_09: sub bl, '0' num_check: cmp rbx, r9 jge num_false mul r9 add rax, rbx inc rsi dec rcx jnz num0 or r10, r10 push rax push 1 jmp _next num_false: xor rcx, rcx push rcx jmp _next
O código fonte está ficando maior, então eu o trago aqui pela última vez.Agora, seu local de residência estará no github: https://github.com/hal9000cc/forth64No mesmo local, na pasta bin, você pode encontrar a versão já compilada para Linux x64. Quem tem Linux, você pode baixar e executar.E quem tem Windows - você pode instalar o WSL (Windows Subsystem for Linux). Eu estava saindo para as férias e fiz exatamente isso. Acabou sendo muito simples, demorou cerca de 5 minutos, houve apenas um momento, não foi iniciado imediatamente, o subsistema teve que ser "ativado" através do comando PowerShell. Seguiu o link da mensagem de erro, executou o comando e funcionou.Mas também há uma maneira de os índios reais executarem tudo no Windows :) Não é difícil fazer isso, basta refazer algumas palavras que interagem com o sistema.Isso é tudo! Da próxima vez, executaremos o compilador.Haverá uma oportunidade de compilar novas palavras, haverá condições, ciclos. Na verdade, será possível escrever em um forte mais ou menos padrão, compilá-lo em código de bytes e executá-lo. Bem, será possível realizar testes mais sérios, verificar o desempenho da máquina de bytes.Continuação: Byte-machine para o forte (e não apenas) no nativo americano (parte 4)