Faça com que o montador “z80asm” coloque uma instrução em um endereço de memória conhecido

Dec 30 2020

Estou escrevendo um sistema operacional muito básico para meu computador homebrew Z80. Como um novato absoluto em linguagem assembly, consegui obter um "monitor OS plus memory" que pode mostrar o conteúdo da memória e carregar bytes na RAM. Ao fazer isso, escrevi algumas "rotinas de sistema" para fazer a interface de alguns dispositivos de E / S. Por exemplo, tenho uma rotina "Printc" que lê um byte e desenha o caractere ASCII correspondente na tela.

Isso está funcionando com o código construído pelo montador porque o montador decide onde colocar o primeiro byte da rotina e usa esse endereço quando encontra um comando jp com o mesmo rótulo.

Agora, eu gostaria de chamar a rotina Printc de um programa carregado dinamicamente. Sou capaz de dizer onde o montador colocou o primeiro byte da rotina na ROM graças ao -lsinalizador, que produz uma saída contendo:

...
Print:    equ $043a Printc: equ $043e
Readc:    equ $0442 Readline: equ $0446
...

Agora posso escrever um programa como este:

ld a, 0x50     ; ASCII code for P
call 0x043e    ; Calls Printc

Este programa imprime a letra P com sucesso: chamei minha rotina Printc usando seu endereço de memória.

Isso é bom, desde que eu não altere nenhum código assembly que anteceda a declaração Printc em meu "os". Se eu fizer isso, a etiqueta Printc será atribuída a outro endereço e meu programa existente deixará de funcionar.

Qual é a solução canônica para esse tipo de problema? O único que me vem à cabeça é criar uma "tabela de salto" no início do meu código assembly, antes de qualquer importação, com a lista de chamadas do sistema, na esperança de que obtenham sempre o mesmo endereço. Algo como:

...
; System routines
Sys_Print:
call Print
ret
Sys_Printc:
call Printc
ret
.... and so on

Mas isso parece bem hack ... É possível instruir o z80asmmontador para colocar a primeira instrução da rotina em um endereço de memória decidido por mim?

Respostas

10 Raffzahn Dec 30 2020 at 04:31

Qual é a solução canônica para esse tipo de problema?

Não existe uma solução canônica, mas muitas variantes, todas utilizáveis.

O único que me vem à mente é criar uma "mesa de salto" no início

O que é perfeito e bom. Exceto, normalmente seria usado saltos em vez de chamadas para reduzir o comprimento do código, acelerar a execução e reduzir a carga da pilha.


JUMP_TABLE:
PRINT    JP  _I_PRINT    ; First Function
READC    JP  _I_READC    ; Second Function
...

Mas isso parece bem hackeado ...

Não, muitos sistemas 8080 e Z80 funcionam assim.

O principal passo à frente é que todos os pontos de entrada estão em um único local e sequência definidos.

É possível instruir o montador z80asm para colocar a primeira instrução da rotina em um endereço de memória decidido por mim?

Claro, use um ORG para colocá-lo no endereço que quiser (* 1). Mas isso seria hackish ou, pelo menos, não muito voltado para o futuro. Ter essa tabela de salto em um endereço definido é um grande começo. Claro que ocupa algum espaço. Três bytes por entrada, mas apenas dois sendo o endereço. Não seria melhor apenas fazer uma tabela de endereços? Como:

SYS_TABLE:
         DW    _I_PRINT    ; First Function
         DW    _I_READC    ; Second Function

Chamar uma função seria como

         LD    HL, (SYS_TABLE+0)   ; Load Address of First Function - PRINT
         JP    (HL)                ; Do it

Isso pode ser facilmente combinado com uma espécie de seletor de função:

SYS_ENTRY:
         PUSH  HL
         LD    H,0
         LD    L,A
         ADD   HL,HL
         ADD   HL,SYS_TABLE
         JP    (HL)

Agora, até mesmo a tabela de salto pode ser movida em ROM (ou RAM) conforme necessário.

Chama-lo seria usando um número de função - como muitos sistemas operacionais fazem - simplesmente coloque o número da função em A e chame o ponto de entrada do sistema padrão (SYS_ENTRY).

         LD    A,0   ; Print
         CALL  SYS_ENTRY

É claro que fica mais legível se o sistema operacional fornecer um conjunto de equações para os números das funções :)

Até agora, o programa carregado ainda precisa saber o endereço da tabela (SYS_TABLE) ou o ponto de entrada para o seletor (SYS_ENTRY). O próximo nível de abstração moveria seu endereço para um local definido, como 0100h, melhor talvez na forma de um JP, então qualquer programa de usuário sempre chama aquele endereço fixo (0100h), não importa se seu sistema operacional está em ROM ou RAM ou em qualquer outro lugar.

E sim, se isso parece familiar, é, já que é a mesma maneira que o CP / M trata as chamadas do sistema, ou o MS-DOS.

Falando em MS-DOS, ele fornece uma forma adicional (e mais conhecida) de chamar uma função do sistema operacional, as chamadas interrupções de software, como o conhecido INT 21h. E há algo bastante semelhante que o Z80 (e 8080 antes) oferece: Um conjunto de oito vetores ReSTart distintos (0/8/16 / ...). O reinício 0 é reservado para reinicialização, todos os outros podem ser usados. Então, por que não usar o segundo (RST 8h) no seu SO? As chamadas de função seriam assim:

         LD    A,0   ; Print
         RST   8h

Agora o código do programa do usuário está o mais separado possível da estrutura do SO e do layout de memória - sem a necessidade de qualquer relocador ou qualquer outra coisa. A melhor parte é que, com um pouco de manipulação, todo o seletor se encaixa nos 8 bytes disponíveis, tornando a codificação ideal.


Uma pequena sugestão:

Se você for para qualquer um desses modelos, certifique-se de que a primeira função (0) do seu sistema operacional será uma chamada fornecendo informações sobre o sistema operacional, para que os programas possam verificar a compatibilidade. Devem ser devolvidos pelo menos dois valores básicos:

  • Número de lançamento ABI
  • Número máximo de funções suportadas.

O número da versão ABI pode ou não ser igual ao número da versão, mas não é obrigatório. Deve ser aumentado a cada mudança de API. Juntamente com o número máximo de funções suportadas, essas informações podem ser usadas por um programa do usuário para encerrar normalmente em caso de incompatibilidade - em vez de travar no meio do caminho. Para luxo, a função também pode retornar um ponteiro para um

  • Estrutura contendo mais informações sobre o sistema operacional como
    • nome / versão legível
    • Endereços de várias fontes
    • pontos de entrada 'especiais'
    • Informações da máquina, como tamanho da RAM
    • interfaces disponíveis, etc.

Apenas dizendo...


* 1 - E não, a não ser que alguns possam supor, o ORG nunca deve adicionar preenchimento ou algo semelhante por conta própria. Montadores fazendo isso são uma escolha ruim. A organização deve apenas alterar o nível de endereço, não definir o que está em qualquer área 'pulado'. Fazer isso adiciona muitos níveis de erros potenciais - pelo menos assim que algum uso avançado de ORG é feito - acredite, ORG é uma ferramenta muito versátil ao fazer estruturas complexas.

Além disso, preencher as áreas 'vazias' com algum preenchimento resultará neste preenchimento sendo parte do programa em vez de memória intacta, retirando uma ferramenta principal para patches posteriores: o espaço EPROM não inicializado. Simplesmente não definindo e não carregando essas áreas, eles permanecerão em qualquer estado de limpeza (todos os no caso de EPROM) e podem ser programados posteriormente - por exemplo, para manter algum código durante a depuração, ou para aplicar uma correção automática sem o necessidade de programação de novos dispositivos.

Portanto, a memória indefinida deveria ser apenas isso, indefinida. E é por isso que mesmo os primeiros formatos de saída / carregador de assembler (pense em Motorola SREC ou Intel HEX ) usados ​​para entrega de programas a qualquer coisa, desde a fabricação de ROM até programas de usuário, suportavam uma forma de deixar áreas de fora.

Resumindo a história: se alguém quiser que seja preenchido, isso deve ser feito com expectativa. z80asm faz isso direito.

12 WillHartung Dec 30 2020 at 04:55

O problema com o Z80ASM especificamente é que ele pega a entrada do assembly e gera um arquivo binário estático. Isso é bom e ruim.

Em sistemas "normais", a atribuição de endereço é, inevitavelmente, responsabilidade do vinculador, não do montador. Mas os montadores são simples o suficiente para que muitos ignorem esse aspecto do ciclo de construção.

Como o Z80ASM gera imagens binárias literais, em vez de arquivos "objeto", ele não precisa de um vinculador. Mas também não permitirá que você necessariamente faça o que deseja.

Considere a onipresente diretiva ORG.

ORG diz ao montador qual é o endereço inicial (origem - portanto ORG) para o próximo código de montagem.

Isso significa que se você fizer isso:

    ORG 0x100
L1: jp L1

O montador montará a instrução JP para JUMP para o endereço de 0x100 (L1).

MAS, quando cuspir o arquivo binário, o arquivo terá apenas 3 bytes. A instrução de salto, seguida por 0x100 no formato binário. Não há nada neste arquivo que diga, bem, nada, que ele deve ser carregado em 0x100 para "funcionar". Essa informação está faltando.

Se você fizer:

    ORG 0x100
L1: jp L2

    ORG 0x200
L2: jp L1

Isso produzirá um arquivo de 6 bytes. Isso vai colocar essas duas instruções do JP um após o outro. A única coisa que a instrução ORG faz é informar quais devem ser os rótulos. Isso não é o que você esperaria.

Portanto, simplesmente adicionar um ORG ao seu arquivo não fará o que você deseja, a menos que você tenha um método alternativo para carregar o código no local específico em que deseja que seu código esteja.

A única maneira de fazer isso com o Z80ASM pronto para usar é preencher seu arquivo de saída com blocos de bytes, espaço vazio, que preencherão o binário para colocar seu código no lugar certo.

Normalmente, é isso que o vinculador faz por você. O trabalho do vinculador é pegar suas partes díspares de código e criar uma imagem binária resultante. Ele faz tudo isso por você.

No meu montador, que não usava um vinculador, ele produziu um formato de arquivo Intel HEX que inclui o endereço real de cada bloco de dados.

Portanto, para o exemplo anterior, seriam criados dois registros. Um destinado a 0x100, o outro a 0x200, e então o programa de carregamento hexadecimal colocaria as coisas no lugar certo. Esta é outra alternativa, mas o Z80ASM também não parece oferecer suporte a ela.

Então.

O Z80ASM é ótimo se você estiver criando imagens ROM a partir de, digamos, arbitrariamente, 0x1000. Você faria o ORG, obteria um binário resultante e baixaria todo o arquivo gravado em uma EPROM. É perfeito para isso.

Mas para o que você deseja fazer, você precisará preencher seu código para mover suas rotinas para os lugares certos, ou criar algum outro esquema de carregador para manifestar isso para você.

5 GeorgePhillips Dec 30 2020 at 03:16

A orgdiretiva deve fazer especificamente o que você pede. No entanto, z80asm é um pouco simplista em seu formato de saída. Em vez disso, você pode usar dspara colocar rotinas em endereços específicos:

        ds     0x1000
printc:
        ...
        ret

        ds     0x1100-$
readc:
        ...
        ret

Isso sempre será colocado printcem 0x1000 e readcem 0x1100. Existem muitas desvantagens. Deve printccrescer mais que 0x100 o programa não montará e você precisará quebrar de printcalguma forma e colocar o código extra em outro lugar. Por essa e outras razões, uma mesa de salto em um local fixo na memória é mais fácil de gerenciar e mais flexível:

           ds    0x100
v_printc:  jp    printc
v_readc:   jp    readc
           ...

Outra técnica é usar um único ponto de entrada e escolher a função usando um valor no Aregistro. Isso será pelo menos um pouco mais lento, mas significa que apenas um único ponto de entrada precisa ser mantido conforme o sistema operacional muda.

E em vez de fazer um CALLpara o ponto de entrada, coloque-o em um dos RSTlocais especiais (0, 8, 0x10, 0x18, 0x20, 0x28, 0x30, 0x38) onde você pode usar RST 0x18como uma chamada de byte único para o local de memória 0x18. Normalmente RST 0e RST 0x38são evitados, pois são os locais do ponto de entrada do pwoer-on e do manipulador do modelo de interrupção 1, respectivamente.