O objetivo desta página é ilustrar conceitos na área de Arquitetura de Computadores estudados na disciplina de Sistemas Operacionais Embarcados (TE355). Os programas exemplo foram elaborados utilizando a plataforma Linux e o debugador gdb. No entanto, se você não tiver acesso a um sistema Linux, é possível compilar e debugar on line utilizando o site https://www.onlinegdb.com/
Considere o seguinte programa:
section .data
; Declara uma variável na seção de dados.
; "my_data" é o rótulo, "dd" define um double-word (32 bits)
; e "12345" é o valor inicial.
my_data dd 12345
section .text
global _start
_start:
; Mover o valor 100 para o registrador EAX
mov eax, 100
; Mover o valor de EAX para o endereço de memória "my_data"
mov [my_data], eax
; Mover o valor da memória "my_data" para o registrador EBX
mov ebx, [my_data]
; --- Saída do programa (syscall para sair) ---
; sys_exit é a chamada de sistema 1
mov eax, 1
; Código de retorno 0 (sucesso)
xor ebx, ebx
int 0x80
Grave o arquivo como mem.asm.
Para compilar, utilize:
# 1. Compila o arquivo .asm
nasm -f elf32 mem.asm -o mem.o
# 2. Linka o arquivo objeto
ld -m elf_i386 mem.o -o mem
# 3. Executa o programa
./mem
# 4. Verifica o código de saída
echo $?
Considere o seguinte programa:
section .text
global _start
_start:
; Mover o valor 10 para o registrador EAX
mov eax, 10
; Mover o valor 5 para o registrador EBX
mov ebx, 5
; --- Adição ---
; Adicionar o valor de EBX a EAX (EAX = 10 + 5 = 15)
add eax, ebx
; --- Subtração ---
; Mover 20 para ECX
mov ecx, 20
; Subtrair 5 de ECX (ECX = 20 - 5 = 15)
sub ecx, 5
; --- Multiplicação ---
; Mover 4 para EDX
mov edx, 4
; Multiplicar EAX por EDX. O resultado de 32 bits fica em EAX
; EAX = 15 * 4 = 60
imul edx
; --- Saída do programa ---
mov eax, 1
xor ebx, ebx
int 0x80
Grave o arquivo como ula.asm.
Para compilar, utilize:
# 1. Compila o arquivo .asm
nasm -f elf32 ula.asm -o ula.o
# 2. Linka o arquivo objeto
ld -m elf_i386 ula.o -o ula
# 3. Executa o programa
./ula
# 4. Verifica o código de saída
echo $?
Note que não é possível em um sistema operacional moderno realizar operações de E/S diretamente no hardware. O hardware estará em modo protegido! Em vez disso, usamos chamadas de sistema (syscalls) para que o kernel realize a operação em nosso nome.
Considere o seguinte programa:
section .data
; A string a ser impressa. "hello" é o rótulo
msg db 'Hello, World!', 0xa ; 0xa é o caractere de nova linha (line feed)
; O comprimento da string
len equ $ - msg
section .text
global _start
_start:
; --- Chamada de sistema sys_write ---
; sys_write é a chamada de sistema 4
mov eax, 4
; O descritor de arquivo (file descriptor) 1 é a saída padrão (stdout)
mov ebx, 1
; O endereço do buffer (nossa string)
mov ecx, msg
; O comprimento do buffer (o valor que definimos como 'len')
mov edx, len
; Chama o kernel para executar a operação
int 0x80
; --- Saída do programa ---
mov eax, 1
xor ebx, ebx
int 0x80
Grave o arquivo como es.asm.
Para compilar, utilize:
# 1. Compila o arquivo .asm
nasm -f elf32 es.asm -o es.o
# 2. Linka o arquivo objeto
ld -m elf_i386 es.o -o es
# 3. Executa o programa
./es
# 4. Verifica o código de saída
echo $?
O programa a seguir configura a porta serial (baud rate, etc.) e envia um único byte para ela, escrevendo diretamente no hardware. Atualmente, a execução da instrução OUT vai causar um problema, de forma que só é possível executar este programa no modo real do hardware.
Apesar de não ser possível executar o código abaixo diretamente em um ambiente Linux comum, ele é útil para entender a lógica e as instruções que seriam usadas para interagir com a porta serial.
A porta serial COM1 é acessada através de endereços de E/S (I/O ports) específicos. Para a COM1, o endereço base é 0x3F8. Para se comunicar com a porta, usamos as instruções OUT e IN da arquitetura x86.
out dx, al: Escreve o byte contido no registrador AL para a porta de E/S cujo endereço está em DX.in al, dx: Lê um byte da porta de E/S cujo endereço está em DX e o armazena no registrador AL.A porta serial tem vários registradores que são acessados a partir do seu endereço base (0x3F8):
0x3F8 (Base): Data Register (registrador de dados). Para escrita, envia dados. Para leitura, lê dados recebidos.0x3F9 (Base + 1): Interrupt Enable Register (habilita interrupções).0x3FA (Base + 2): Interrupt Identification Register (identificador de interrupção).0x3FB (Base + 3): Line Control Register (controle de linha), onde se configura a velocidade (baud rate), paridade, etc.0x3FD (Base + 5): Line Status Register (estado da linha), que indica se a porta está pronta para receber dados.O programa é o seguinte:
section .text
global _start
_start:
; Endereço base da COM1
mov dx, 0x3F8
; --- 1. Configurar a porta serial ---
; Habilitar o modo de divisor para configurar o baud rate
; Linha de controle no endereço base+3 (0x3FB)
mov al, 0x80 ; 0x80 = 10000000b (set DLAB)
mov dx, 0x3FB
out dx, al
; Enviar a taxa de 115200 bps
; Divisor para 115200 bps é 1 (115200 / 1 = 115200)
; O divisor é um valor de 16 bits, então enviamos em duas partes.
; Byte menos significativo (LCR em 0x3F8)
mov al, 0x01
mov dx, 0x3F8
out dx, al
; Byte mais significativo (LCR em 0x3F9)
xor al, al ; al = 0
mov dx, 0x3F9
out dx, al
; Resetar o bit DLAB no registrador de controle de linha
mov al, 0x03 ; 0x03 = 00000011b (8N1)
mov dx, 0x3FB
out dx, al
; --- 2. Esperar que a porta esteja pronta ---
.wait_for_transmit:
mov dx, 0x3FD ; Endereço do Line Status Register
in al, dx
; Verificar se o bit 5 (Transmitter Holding Register Empty) está setado
test al, 0x20
jz .wait_for_transmit
; --- 3. Enviar um byte para a porta ---
mov al, 'A' ; O byte a ser enviado (o caractere 'A')
mov dx, 0x3F8
out dx, al
; --- Saída do programa (syscall para sair) ---
mov eax, 1
xor ebx, ebx
int 0x80
Ao executar este programa, o SO mostra uma mensagem "Segmentation fault (core dumped)", que é um indicativo genérico de falha. No entanto, o erro real foi uma "Illegal hardware access" produzida pelo hardware, que levou o SO a abortar a execução do programa.
Para depurar os programas gerados, o GDB (GNU Debugger) é a ferramenta padrão e mais poderosa no ambiente Linux. Para usá-lo, você precisa gerar o programa executável com informações de depuração. Depois, basta seguir os passos abaixo para iniciar a depuração.
Inicie o GDB com o executável:
gdb ./prog
Isso abrirá o GDB. Você verá um prompt (gdb).
O rótulo _start é a entrada do seu programa. Defina um ponto de parada (breakpoint) para que o GDB pause a execução logo no início.
(gdb) b _start
(A forma curta de breakpoint é b).
(gdb) run
(A forma curta de run é r). O GDB vai executar o programa até o _start e parar. Ele informará que parou no breakpoint.
Para ver o código Assembly próximo ao ponto de execução atual, use:
(gdb) disassemble _start
Use o comando nexti (next instruction) ou ni para executar a próxima instrução.
(gdb) ni
Isso executará a instrução corrente.
Para ver o estado de todos os registradores, use:
(gdb) info registers
Continue usando ni para avançar e info registers para observar a mudança de estado dos registradores.
Quando terminar de explorar, você pode sair do GDB com o comando quit.
(gdb) quit
| Comando | Descrição |
|---|---|
b <label> |
Define um breakpoint no rótulo (ex: b _start). |
r |
Executa o programa até o primeiro breakpoint. |
ni |
Executa a próxima instrução de Assembly. |
si |
Executa a próxima instrução, "entrando" em chamadas de função (útil para sub-rotinas). |
i r |
Mostra o valor de todos os registradores. |
x/<n>x <endereço> |
Examina a memória no endereço. n é o número de bytes a exibir. |
c |
Continua a execução até o próximo breakpoint. |
q |
Sai do GDB. |