Na década de 80, uma empresa desconhecida chamada Binary Systems publicou o jogo Starflight. O jogo coloca o jogador no papel de um capitão de uma nave enviado para explorar a galáxia. Não existe um caminho definido, permitindo aos jogadores alternar livremente entre mineração, combate entre navios e diplomacia alienígena. O enredo mais amplo do jogo surge lentamente, à medida que o jogador descobre que uma antiga raça de seres está fazendo com que as estrelas brilhem e destruam todas as criaturas vivas. O jogo foi amplamente elogiado pela crítica contemporânea e moderna e é um dos primeiros exemplos de jogo sandbox. O jogo influenciou o design de vários outros jogos durante décadas após seu lançamento.
Para saber mais sobre o jogo acesse os seguintes links:
Você pode comprar o jogo no GoG
A primeira vez que ouvi falar do jogo, quis jogá-lo. No entanto, eu era muito jovem e não sabia falar inglês. 20 depois tentei novamente e foi uma experiência muito agradável. A exploração é divertida, o enredo é épico e termina com uma surpresa, que é uma das melhores que já experimentei. Claro, o jogo não envelheceu bem, mas você pode sentir a devoção dos desenvolvedores ao jogo. Há um aspecto artístico neste jogo, bem como a atenção de um artesão aos detalhes.
Por mais que jogar este jogo verdadeiramente incrível seja divertido, a engenharia reversa deste jogo também o é. Você segue os passos dos desenvolvedores e vivencia seus processos de pensamento como se fosse o ano de 1985 novamente. Para este jogo espere o inesperado. Normalmente, quando você faz engenharia reversa em um jogo tão antigo, você recebe dez milhares de linhas de código assembler puro, que você pode analisar com ferramentas usuais como o IDA Pro. Mas não desta vez. Na verdade, para este jogo você pode jogar fora as ferramentas usuais. Eles são inúteis. Você está sozinho. A razão é que Starflight foi escrito em Forth, uma língua que eu mal conhecia.
Em seguida está a linguagem com o minimalismo máximo em relação à sintaxe. Não existe mais sintaxe do que o espaço entre “palavras”. Você pode escrever um leitor e intérprete Forth basicamente em algumas linhas de código.
Em uma linguagem moderna você escreve algo como
print ( 2 + 3 )
para imprimir o resultado de 2+3. Em Forth, no entanto, é assim.
2 3 + .
A seguir está uma máquina de pilha, com notação polonesa reversa. A interpretação é a seguinte
A sintaxe é simples e o interpretador é simples. "2", "3", "+" e "." são chamadas apenas de "palavras". Não há distinção sintática entre dados e código. Certamente uma linguagem que correspondeu às limitações dos primeiros computadores domésticos.
Quando você disseca o executável STARFLT.COM, ele revela alguns detalhes internos fantásticos
Conforme explicado acima, Forth é uma máquina de pilha. Como mecânica de codificação, ele usa threading indireto, um método muito eficiente em termos de espaço para armazenar seu código compilado. O código encadeado tem uma forma que consiste essencialmente em chamadas para sub-rotinas. O threading indireto usa ponteiros para locais que, por sua vez, apontam para o código de máquina.
Digamos que seu ponteiro de instrução aponte para o endereço 0x1000 e contenha o valor de 16 bits Read16(0x1000)=0x0f72.
0x1000 : dw 0x0f72
O valor 0x0f72 é o equivalente codificado da quarta palavra '+'. Lembre-se da descrição acima. A palavra '+' exibe as duas últimas entradas da pilha, soma-as e coloca o resultado de volta no topo da pilha. De acordo com o threading indireto, esse valor de 16 bits 0x0f72 é um ponteiro para um local que, por sua vez, aponta para o código de máquina. Ao ler o conteúdo da memória Read16(0x0f72) você obtém o ponteiro para 0x0f74. E, de fato, quando você olha para este local de memória e o desmonta, você recebe o seguinte
0x0f72 : dw 0x0f74
0x0f74 : pop ax
0x0f75 : pop bx
0x0f76 : add ax , bx
0x0f78 : push ax
0x0f79 : lodsw
0x0f7a : mov bx , ax
0x0f7c : jmp word ptr [ bx ]
As primeiras quatro instruções realizam exatamente as operações que a palavra “+” deveria realizar. As últimas três instruções assembler começando em "lodsw" aumentam o ponteiro da instrução e saltam para o próximo código.
Vamos continuar. Agora o ponteiro de instrução aponta para 0x1002
0x1002 : dw 0x53a3
A leitura do endereço 0x53a3 revela
0x53a3 : dw 0x1d29
0x53a5 : dw 0x0001
e o código correspondente
0x1d29 : inc bx
0x1d2a : inc bx
0x1d2b : push bx
0x1d2c : lodsw
0x1d2d : mov bx , ax
0x1d2f : jmp word ptr [ bx ]
Neste momento o registrador bx contém o endereço da palavra 0x53a3. Portanto, este código apenas coloca o endereço 0x53a5 no topo da pilha. O que fizemos foi fornecer ao programa um ponteiro para uma variável. A variável tem o conteúdo 0x0001. A quarta palavra '@' retiraria o endereço da pilha, leria seu conteúdo e o colocaria de volta na pilha.
Até agora consegui identificar 6.256 palavras que contêm código ou dados.
E isso é tudo que você precisa saber sobre a estrutura do código. Como você pode ver, esta pode ser uma codificação eficiente em termos de espaço, mas em termos de velocidade é uma catástrofe. A cada poucas instruções de código de máquina, você precisa pular para um bloco de código diferente.
O equivalente ao threading indireto em C seria assim.
uint16_t instruction_pointer = start_of_program_pointer ;
void Call ( uint16_t word_adress )
{
// the first two byte of the word's address contain
// the address of the corresponding code, which must be executed for this word
uint16_t code_address = Read16 ( word_address );
switch ( code_address )
{
.
.
.
case 0x0f74 : // word '+'
Push16 ( Pop16 () + Pop16 ());
break ;
.
.
.
}
}
void Run ()
{
while ( 1 )
{
uint16_t word_address = Read16 ( instruction_pointer );
instruction_pointer += 2 ;
Call ( word_address );
}
}
O código executado para uma palavra específica tem acesso a 5 variáveis principais (16 bits)
A desmontagem transpila o código FORTH em código estilo C. A maior parte do código transpilado é compilado. Para entender o que o programa faz, dê uma olhada na tabela a seguir. Ele pega o "bytecode" (que são principalmente ponteiros de 16 bits) como entrada e o transforma em C.
Quarto código:
: .C ( -- )
Display context stack contents.
CR CDEPTH IF CXSP @ 3 + END-CX
DO I 1.5@ .DRJ -3 +LOOP
ELSE ." MT STK"
THEN CR ;
EXIT
Transformação:
Ponteiros de 16 bits | ADIANTE | C |
---|---|---|
: .C (-) | void DrawC() { | |
unsigned short int i, imax; | ||
0x0642 | CR | Exec("CR"); |
0x75d5 | PROFUNDIDADE DO CD | CDEPTH(); |
0x15fa 0x0020 | SE | if (Pop() != 0) { |
0x54ae | CXSP | Push(Read16(pp_CXSP) + 3); |
0xbae | @ | |
0x3b73 | 3 | |
0x0f72 | + | |
0x4ffd | FIM-CX | Push(Read16(cc_END_dash_CX)); |
0x15b8 | FAZER | i = Pop(); |
imax = Pop(); | ||
do { | ||
0x50e0 | EU | Push(i); |
0x4995 | 1,5@ | _1_dot_5_at_(); |
0x81d5 | .DRJ | DrawDRJ(); |
0x175d 0xfffd | -3 | Push(-3); |
0x155c 0xffff | +LOOP | int step = Pop(); |
i += step; | ||
if (((step>=0) && (i>=imax)) || ((step<0) && (i<=imax))) break; | ||
} while(1); | ||
0x1660 0x000b | OUTRO | } else { |
0x1bdc | "MT STK" | PRINT("MT STK", 6); |
0x06 | ||
0x4d | 'M' | |
0x54 | 'T' | |
0x20 | ' ' | |
0x53 | 'S' | |
0x54 | 'T' | |
0x4b | 'K' | |
ENTÃO | } | |
0x0642 | CR | Exec("CR"); |
0x1690 | SAÍDA | } |
O jogo vem em 3 arquivos
Conteúdo de STARA.com
entrada | tamanho | descrição |
---|---|---|
DIRETÓRIO | 4096 | contém diretório de STARA e STARB |
ELO-CPIC | 4816 | |
GAZ-CPIC | 3120 | |
MEC-CPIC | 2848 | |
MYS-CPIC | 6064 | |
NOM-CPIC | 1136 | |
SPE-CPIC | 1888 | |
THR-CPIC | 2480 | |
VEL-CPIC | 4672 | |
VPR-CPIC | 1248 | |
MIN-CPIC | 2096 | |
RESPINGO | 16384 | Foto |
MED-PIC | 2048 | Foto |
FASES | 6144 | |
HUM-PIC | 480 | Foto |
VEL-PIC | 432 | Foto |
THR-PIC | 272 | Foto |
ELO-PIC | 608 | Foto |
E-PIC | 640 | Foto |
SALVAR | 124.000 | |
MÚSICA | 4960 | Sobreposição de código |
TERRA | 1152 | Mapa do planeta terra |
GALÁXIA | 6304 | |
CRÉDITOS | 16384 | foto |
COP-CPIC | 2928 | |
FONTES | 768 | |
CGA | 3600 | Rotinas de código de máquina para a placa gráfica CGA |
EGA | 3600 | Rotinas de código de máquina para a placa gráfica EGA |
Conteúdo de STARB.COM
entrada | tamanho | descrição |
---|---|---|
DIRETÓRIO | 4096 | contém diretório de STARA e STARB |
EXEMPLO | 150528 | Estrutura em árvore com a maior parte do conteúdo do jogo |
CAIXA | 1024 | Mesa |
BANCO-TRANS | 144 | Mesa |
MEMBRO DA TRIPULAÇÃO | 128 | Mesa |
NAVIO | 1936 | Mesa |
ELEMENTO | 544 | Mesa |
ARTEFATO | 1584 | Mesa |
PLANETA | 1360 | Mesa |
AMOSTRA | 448 | Mesa |
BIO-DADOS | 448 | Mesa |
TPORT-PIC | 2416 | Foto |
BPORT-PIC | 3984 | Foto |
ANALISAR-TEXTO | 3200 | Mesa |
BOTÕES | 944 | Mesa |
ÍCONE1:1 | 912 | |
ÍCONE 1:2 | 912 | |
ÍCONE 1:4 | 912 | |
NOME DO ÍCONE | 736 | |
DPART-OV | 1552 | Sobreposição de código |
REGIÕES | 176 | Mesa |
CRIATURA | 17024 | Mesa |
CHKFLIGHT-OV | 960 | Sobreposição de código |
FRACT-OV | 4640 | Sobreposição de código |
ICONP-OV | 832 | Sobreposição de código |
SITE-OV | 1888 | Sobreposição de código |
HIPERMSG-OV | 4112 | Sobreposição de código |
GPOLY | 368 | |
FACETA | 288 | |
VÉRTICE | 416 | |
BLT-OV | 864 | Sobreposição de código |
MISC-OV | 1440 | Sobreposição de código |
BANCO-OV | 1520 | Sobreposição de código |
ASSCREW-OV | 2800 | Sobreposição de código |
PESSOAL-OV | 4192 | Sobreposição de código |
SHIPGRPH-OV | 2112 | Sobreposição de código |
CONFIG-OV | 3072 | Sobreposição de código |
TDEPOT-OV | 4800 | Sobreposição de código |
PORTOMENU-OV | 3120 | Sobreposição de código |
VITA-OV | 3552 | Sobreposição de código |
HP-OV | 4832 | Sobreposição de código |
LP-OV | 5280 | Sobreposição de código |
ENVIADO-OV | 4784 | Sobreposição de código |
TV-OV | 3472 | Sobreposição de código |
COM-OV | 7232 | Sobreposição de código |
COMMSPEC-OV | 2864 | Sobreposição de código |
SEMENTE-OV | 2400 | Sobreposição de código |
LISTICONS | 720 | Sobreposição de código |
MOVE-OV | 3808 | Sobreposição de código |
ENGENHEIRO | 2320 | Sobreposição de código |
DOUTOR | 1280 | Sobreposição de código |
ÓRBITA-OV | 6640 | Sobreposição de código |
CAPITÃO | 5952 | Sobreposição de código |
CIÊNCIA | 3952 | Sobreposição de código |
NAVEGADOR | 880 | Sobreposição de código |
BOTÕES DE NAVIO | 1984 | |
MAPA-OV | 4160 | Sobreposição de código |
HIPER-OV | 7168 | Sobreposição de código |
ANALISAR-OV | 2560 | Sobreposição de código |
LANÇAMENTO-OV | 1360 | Sobreposição de código |
EFEITO DE FLUXO | 464 | |
OP-OV | 4400 | Sobreposição de código |
ITENS-OV | 6016 | Sobreposição de código |
LSYSICON | 752 | |
MSYSICON | 448 | |
SSYSICON | 176 | |
COMPORTAMENTO-OV | 5360 | |
CMAP | 1008 | |
INSTALAR | 800 | |
CURA-OV | 1232 | Sobreposição de código |
REPARO-OV | 1696 | Sobreposição de código |
JOGO-OV | 5920 | Sobreposição de código |
PLSET-OV | 2400 | Sobreposição de código |
MAPAS-OV | 2240 | Sobreposição de código |
VES-BLT | 4528 | |
TEMPESTADE-OV | 1232 | Sobreposição de código |
COMPOSTOS | 176 | Mesa |
IT-OV | 1936 | Sobreposição de código |
COMBAT-OV | 6192 | Sobreposição de código |
DANO-OV | 2752 | Sobreposição de código |
TERRA-OV | 1088 | Sobreposição de código |
PSTATS | 64 | Mesa |
STP-OV | 1440 | Sobreposição de código |
Coloque os arquivos do jogo Starflight original nas pastas starflt1-in
e starflt2-in
e execute make
. Você deve obter dois executáveis ( disasOV1
e disasOV2
), que produzem o conteúdo nas pastas starflt1-out
e starflt2-out
. A saída gerada faz parte deste repositório.