Debugger do openMSX
Você está em: MarMSX >> Cursos >> Assembly Z80
Que tal elevar o nível de depuração de seus programas em Assembly? O debugger do openMSX permite o amplo acesso aos registradores do MSX e o controle da máquina.
Afinal o que é um depurador?
Um depurador, ou debugger em inglês, é um programa que permite a análise do funcionamento interno de um programa, de modo que seja possível executar as operações passo a passo e que também seja possível obter o estado da máquina, com o objetivo de permitir ao analista observar o comportamento do programa e identificar possíveis erros de execução.
No tutorial do RSCII, podemos encontrar um exemplo de depuração de programa na seção "simulador passo a passo". Nesse exemplo, criamos um programa em Assembly e executamos o depurador através da instrução SI.
Apesar desse depurador ser poderoso, há certas limitações quanto ao seu uso. Por exemplo, ele não permite a troca dos slots 1 e 3 de memória, uma vez que na página 1 se encontra o programa RSCII e a página 3 contém as variáveis de sistema.
Já o depurador do openMSX está fora da máquina virtual MSX, podendo assim ter acesso irrestrito aos estados do MSX, desde que previstos pelos seus desenvolvedores.
1. Conhecendo o Debugger do openMSX
Após instalar o debugger do openMSX no seu sistema, é só abrir. No Linux, geralmente o chamamos na linha de comando assim:
marmsx@marmsx-desktop:~$ openmsx-debugger
Após o carregamento, a interface do programa aparece na tela.
A interface é composta das seguintes áreas:
- Menu superior
- Botões
- Painéis com os estados da máquina
1.1 Menu superior
O menu superior contém todas as funções do depurador. Ele é composto das seguintes opções:
Opção |
Descrição |
File |
Permite carregar/salvar sessões de depuração. |
System |
Opções de controle do emulador. |
Search |
Ferramenta de busca por locais de memória no visualizador de código. |
View |
Escolhe quais painéis de estado de memória devem aparecer. |
Execute |
Controle de execução do depurador. |
Breakpoint |
Permite a criação/eliminação de pontos de parada do programa na memória. |
Opções para o File:
- New Session - cria uma nova sessão.
- Open Session - abre uma sessão salva.
- Save Session - salva a seção atual diretamente.
- Save Session as - salva a seção atual, com opções de salvamento.
- Quit - sai do debugger.
Opções para o System:
- Connect - conecta o debugger ao emulador openMSX em execução.
- Disconnect - desconecta o debugger do emulador.
- Pause Emulator - pausa o emulador.
- Reboot Emulator - reinicia o emulador.
- Stop Emulator - pára o emulador.
- Symbol Manager - gerenciador de símbolos.
- Preferences - preferências (layout do debugger).
Opções para o Search:
- Goto ... - vai para uma instrução apontada pelo endereço em hexadecimal no code view. Exemplo de endereço: $c000.
Opções para o View:
- CPU Registers - painel com os registradores da CPU.
- CPU Flags - painel com os flags da CPU.
- Stack - painel com a pilha do Z80.
- Slots - painel com os slots do MSX.
- Memory - painel com os memória principal do micro.
- VDP - opções do VDP.
- Registers - registradores do VDP.
- Command Registers - registradores de comando.
- Status Register - registradores de status.
- Bitmapped VRAM - imagem bitmap da VRAM. Só para MSX 2 e 2+.
- Add debuggable viewer - abre um mini debugger com várias opções de depuração.
Opções para o Execute:
- Break - pára a execução do micro na instrução corrente.
- Run - volta a executar o micro.
- Step into - executa a instrução atual e vai para a próxima instrução.
- Step over - executa uma função sem mostrar o passo a passo dela.
- Step out - sai de uma função, executando as instruções restantes.
- Step back - desfaz a execução da instrução atual.
- Run to - roda até um ponto de parada.
Obs: os steps são opções para quando o micro estiver pausado por um comando break ou breakpoint.
Opções para o Breakpoint:
- Toggle - liga/desliga um breakpoint na posição atual.
- Add - adiciona um ponto de parada no endereço especificado.
1.2 Botões
Os botões apresentam as funcionalidades principais do menu, de forma a serem acessadas mais rapidamente.
Temos os seguintes botões:
1.3 Painéis com os estados da máquina
Cada painel de estado da máquina apresenta as informações relativa a memória, flags, registradores etc.
Temos os seguintes painéis:
- Visualizador de código
- Registradores da CPU
- Flags da CPU
- A pilha do Z80
- Configuração de slots
- Memória principal
- VDP
Nos exemplos mais adiante, iremos ver como funcionam alguns desses painéis.
2. Conectando o depurador ao openMSX
Após abrir o depurador, abra também o emulador openMSX.
Apesar de ambos os programas estarem agora em funcionamento, é necessário conectar o depurador ao emulador. Para isso, abra a opção "System", "Connect" do menu ou clique no botão "connect".
Os painéis do depurador passam de inativos a ativos, sendo preenchidos com os dados vindos do emulador. A imagem a seguir apresenta a mudança do depurador, quando conectado ao openMSX.
Ainda não é possível depurar a execução do micro, pois ele está em pleno funcionamento. Veremos como fazer isso na próxima seção.
De forma a ilustrar como podemos ver e editar e modificar certos recursos do MSX no depurador, vamos utilizar a emulação do Expert 1.1 da Gradiente. Ao atingir a tela principal do MSX (e o depurador conectado), vamos abrir a opção "View", "add debuggable viewer". A opção VRAM é a primeira que aparece e e ela que vamos utilizar.
Observe na figura anterior, que na screen 0 o texto do inicio da VRAM corresponde ao texto que aparece na tela.
Podemos editar e alterar o texto em tempo real através do "debuggable viewer". Para isso, clique em um caractere da área de texto (assinalada com o retângulo vermelho) e altere os valores contidos ali. Eu alterei o texto de "versão 1.1" para "versão 2.0".
Conforme dito, essa alteração é refletida imediatamente no emulador.
Os painéis da tela principal do depurador são utilizados somente durante a depuração de programas. É o que veremos na próxima seção.
3. Depurando
Podemos aproveitar o exemplo do tutorial do RSCII para utilizá-lo nesse depurador.
10 ORG &HC000
C000 3E 05 20 LD A,5
C002 06 04 30 LD B,4
C004 80 40 ADD A,B
C005 C9 50 RET
Em Basic, podemos digitar o programa a seguir para colocar o programa na memória.
10 FOR E=&HC000 TO &HC005
20 READ A$
30 POKE E,VAL("&H"+A$)
40 NEXT E
50 DATA 3E,05,06,04,80,C9
Ao digitar o programa, execute com o RUN para que o programa seja de fato colocado na memória.
Utilizando a opção do depurador "Search", "Goto ...", digite o endereço $C000.
Observe o nosso programa na área de código, assinalado com um retângulo vermelho.
Uma observação importante deve ser feita sobre a área de código: a geração dos mnemônicos depende sempre de onde o desassemblador começa seu trabalho. Como ele não começa em &HC000 e devido ao fato de termos instruções em Z80 de um a quatro bytes, a interpretação dessa área de memória pode alterar drasticamente. Por exemplo, se antes da instrução LD A,5 na posição &HC000 tivéssemos um byte relativo a uma instrução de mais de um byte, a seqüência do programa seria alterada.
Dê o comando Basic a seguir e observe a mudança.
POKE &HBFFF,&H3E ↵
Resultado:
Nosso código foi bagunçado. Entretanto, isso ocorre somente no visualizador de código do depurador, pois se executarmos o programa no MSX, iremos sempre apontar o inicio para o local correto. Dessa forma, o MSX irá começar a executar o código em &HC000, ignorando o que vem antes.
De forma a corrigir o problema do visualizador de código, podemos alterar os quatro bytes anterior ao código para &HFF ou &H00.
É possível alterar valores de memória diretamente no painel "Memória principal" do depurador. Para isso, digite o valor FF nas posições de memória necessárias para que o código não fique bagunçado.
Se você preferir, pode digitar o código de máquina de um programa em Assembly diretamente nesse painel.
3.1 Criando breakpoints
Não podemos analisar o código do nosso programa, se não pararmos a execução do MSX. Para isso, estabelecemos pontos de parada ou breakpoints em determinadas instruções do nosso código.
Como desejamos ver toda a execução de nosso código, criamos um breakpoint no endereço &HC000. Para isso, clique com o mouse do lado esquerdo desse endereço, de forma a aparecer uma bolinha vermelha. Outra forma de fazer isso é clicando e selecionando uma determinada instrução (o retângulo sob ela fica laranja), e depois teclar F5. O F5 habilita ou desabilita o breakpoint.
Uma vez criado o breakpoint, é necessário colocar o nosso programa em execução. Para isso, digite em Basic:
DEFUSR=&HC000:X=USR(0) ↵
O emulador irá parar (Break). No visualizador de código, a seta verde indica a próxima instrução a ser executada.
A opção do menu "Excecute", "Step into" irá executar a instrução atual e depois ir para a próxima instrução. Essa opção pode ser executada pressionando-se F7 no depurador.
O estado inicial dos registradores (no meu caso) é o seguinte:
Ao executar a instrução LD A,5, o registrador A é modificado.
A opção "Step back" permite desfazer a execução da última instrução. Assim, se teclarmos F12, voltamos para o endereço &HC000 e o valor dos registradores alterados são recuperados.
A opção "Step over" ou F8 serve para executar direto as funções (rotinas chamadas pela instrução CALL end), sem que vejamos o que ocorre dentro dela.
Como exemplo, crie um breakpoint no endereço &HC000 do depurador e execute o programa abaixo.
10 POKE &HC000,&HCD ' CALL &HC3 (CLS)
20 POKE &HC001,&HC3
30 POKE &HC001,0
40 POKE &HC001,&HC9 ' RET
50 DEFUSR=&HC000 : X=USR(0)
Ao pressionar F8, o depurador executa a rotina CLS (&H00C3) da ROM e retorna imediatamente para a próxima linha do nosso programa (instrução RET). O passo a passo das instruções dessa função não é apresentado, mas os resultados da execução dessa rotina são refletidos em toda a máquina. Observe no emulador que a tela foi apagada.
* C000 CD C3 00 call #00c3
→ C003 C9 ret
Obs: o depurador não possui uma função para saltar a instrução corrente como faz o RSCII.
Se repetirmos a experiência anterior, mas fazendo o "Step into", entraremos na função CLS da ROM. Avance duas instruções. Agora, utilize o "Step out" ou tecle F11. O que aconteceu? O restante da função CLS da ROM foi executado, retornando para a próxima instrução do nosso programa.
* C000 CD C3 00 call #00c3
→ C003 C9 ret
Ao executarmos todo o programa, verificamos que o registrador A contém o valor 9, o que era esperado por nós.
Podemos também executar tudo de uma vez. Para isso, introduzimos um novo breakpoint no local onde desejamos que o programa pare. Nesse caso, coloquei no endereço &HC005, em cima da instrução RET (que não é executada).
Observe na imagem a seguir que a execução está em &HC000 e o ponto de parada é &HC005. Ao teclar F4 ou utilizar a opção "Run to", o programa executa todo o trecho de código compreendido entre o ponto da posição atual e o próximo breakpoint (exclusive).
Para voltar a fazer o MSX rodar normalmente, clique no ícone do "Run".
3.2 Identificando erros
O exemplo a seguir aconteceu comigo devido a um erro durante a elaboração de um programa que fiz. Nesse programa, pude constatar no depurador qual era o problema.
No programa do placar, o código em Assembly que copia uma parte da imagem do buffer para a tela principal continha um erro que fazia a tela toda ser preenchida com lixo.
Há duas rotinas da ROM que fazem a transferência de blocos de memória entre a RAM e VRAM. Cada uma dessas instruções utiliza os registradores DE, BC e HL como parâmetro de entrada de dados. Esse erro acontecia porque como o valor de BC é o mesmo para as duas rotinas, eu havia eliminado a linha 220. Entretanto ambas as rotinas alteram o valor dos registradores após a sua execução, inclusive o BC.
End Cód. Máq. Linha Mnemônicos
C018 11 00 D0 170 INI: LD DE,&HD000 ; Endereço inicial RAM
C01B 01 98 01 180 LD BC,408 ; Tamanho do bloco a copiar
C01E CD 59 00 190 CALL &H59 ; Copia bloco VRAM -> RAM
C021 21 00 D0 200 LD HL,&HD000 ; Endereço inicial da RAM
C024 11 40 0E 210 LD DE,3648 ; Endereço do caractere 200 da tab ASCII
C027 01 98 01 220 LD BC,408 ; Tamanho do bloco a copiar
C02A CD 5C 00 230 CALL &H5C ; Copia bloco VRAM -> RAM
Ao executarmos o programa até a linha 180 (inclusive), o estado dos registradores é o seguinte:
No qual percebemos que os registradores BC, DE e HL contêm os valores esperados antes de chamar a rotina que copia da VRAM para a RAM.
Entretanto, logo após executar a rotina da ROM, observe os valores desses registradores.
Alguns registradores foram alterados, principalmente o BC. Dessa forma, ele deverá ser carregado novamente com o valor &H198, que são 408 bytes. No caso do meu bug, o valor de BC era igual a 0, correspondendo a 65536 bytes a serem copiados.
3.3 O uso da pilha
O programa em Basic a seguir ilustra o uso da pilha do Z80. Crie um breakpoint em &HC000 e depois rode-o.
10 POKE &HC000,&HD5 ' PUSH DE
20 POKE &HC001,&HC9 ' RET
30 DEFUSR=&HC000
40 X=USR(0)
Eu editei e alterei o registrador DE para o valor &H1234. Ao executar a instrução PUSH DE (com o F7), temos a pilha sendo alterada. Como o endereço do SP marca o topo da pilha, o próximo valor é colocado na posição imediatamente anterior.