Eu queria escrever algo básico em montagem no Windows, estou usando NASM, mas não consigo fazer nada funcionar.
Como escrever e compilar hello world sem a ajuda de funções C no Windows?
Eu queria escrever algo básico em montagem no Windows, estou usando NASM, mas não consigo fazer nada funcionar.
Como escrever e compilar hello world sem a ajuda de funções C no Windows?
Respostas:
Chamando libc stdio printf
, implementandoint main(){ return printf(message); }
; ----------------------------------------------------------------------------
; helloworld.asm
;
; This is a Win32 console program that writes "Hello, World" on one line and
; then exits. It needs to be linked with a C library.
; ----------------------------------------------------------------------------
global _main
extern _printf
section .text
_main:
push message
call _printf
add esp, 4
ret
message:
db 'Hello, World', 10, 0
Então corra
nasm -fwin32 helloworld.asm
gcc helloworld.obj
a
Há também o Guia para Iniciantes Clueless para Hello World no Nasm sem o uso de uma biblioteca C. Então, o código ficaria assim.
Código de 16 bits com chamadas de sistema MS-DOS: funciona em emuladores DOS ou em Windows de 32 bits com suporte para NTVDM . Não pode ser executado "diretamente" (de forma transparente) em qualquer Windows de 64 bits, porque um kernel x86-64 não pode usar o modo vm86.
org 100h
mov dx,msg
mov ah,9
int 21h
mov ah,4Ch
int 21h
msg db 'Hello, World!',0Dh,0Ah,'$'
Construa isso em um .com
executável para que seja carregado cs:100h
com todos os registradores de segmento iguais uns aos outros (minúsculo modelo de memória).
Boa sorte.
Este exemplo mostra como ir diretamente para a API do Windows e não vincular à Biblioteca C Standard.
global _main
extern _GetStdHandle@4
extern _WriteFile@20
extern _ExitProcess@4
section .text
_main:
; DWORD bytes;
mov ebp, esp
sub esp, 4
; hStdOut = GetstdHandle( STD_OUTPUT_HANDLE)
push -11
call _GetStdHandle@4
mov ebx, eax
; WriteFile( hstdOut, message, length(message), &bytes, 0);
push 0
lea eax, [ebp-4]
push eax
push (message_end - message)
push message
push ebx
call _WriteFile@20
; ExitProcess(0)
push 0
call _ExitProcess@4
; never here
hlt
message:
db 'Hello, World', 10
message_end:
Para compilar, você precisará de NASM e LINK.EXE (do Visual studio Standard Edition)
nasm -fwin32 hello.asm link / subsistema: console / nodefaultlib / entrada: main hello.obj
gcc hello.obj
Estes são exemplos de Win32 e Win64 usando chamadas de API do Windows. Eles são para MASM em vez de NASM, mas dê uma olhada neles. Você pode encontrar mais detalhes neste artigo.
Isso usa MessageBox em vez de imprimir em stdout.
;---ASM Hello World Win32 MessageBox
.386
.model flat, stdcall
include kernel32.inc
includelib kernel32.lib
include user32.inc
includelib user32.lib
.data
title db 'Win32', 0
msg db 'Hello World', 0
.code
Main:
push 0 ; uType = MB_OK
push offset title ; LPCSTR lpCaption
push offset msg ; LPCSTR lpText
push 0 ; hWnd = HWND_DESKTOP
call MessageBoxA
push eax ; uExitCode = MessageBox(...)
call ExitProcess
End Main
;---ASM Hello World Win64 MessageBox
extrn MessageBoxA: PROC
extrn ExitProcess: PROC
.data
title db 'Win64', 0
msg db 'Hello World!', 0
.code
main proc
sub rsp, 28h
mov rcx, 0 ; hWnd = HWND_DESKTOP
lea rdx, msg ; LPCSTR lpText
lea r8, title ; LPCSTR lpCaption
mov r9d, 0 ; uType = MB_OK
call MessageBoxA
add rsp, 28h
mov ecx, eax ; uExitCode = MessageBox(...)
call ExitProcess
main endp
End
Para montá-los e vinculá-los usando MASM, use para executáveis de 32 bits:
ml.exe [filename] /link /subsystem:windows
/defaultlib:kernel32.lib /defaultlib:user32.lib /entry:Main
ou este para executável de 64 bits:
ml64.exe [filename] /link /subsystem:windows
/defaultlib:kernel32.lib /defaultlib:user32.lib /entry:main
Por que o x64 Windows precisa reservar 28h bytes de espaço de pilha antes de um call
? Isso é 32 bytes (0x20) de espaço de sombra, também conhecido como espaço inicial, conforme exigido pela convenção de chamada. E outros 8 bytes para realinhar a pilha em 16, porque a convenção de chamada exige que o RSP seja alinhado com 16 bytes antes de umcall
. ( main
O chamador do nosso (no código de inicialização CRT) fez isso. O endereço de retorno de 8 bytes significa que RSP está a 8 bytes de distância de um limite de 16 bytes na entrada para uma função.)
O espaço de sombra pode ser usado por uma função para despejar seus argumentos de registro próximo a onde qualquer argumento da pilha (se houver) estaria. UMAsystem call
requer 30h (48 bytes) para também reservar espaço para r10 e r11 além dos 4 registradores mencionados anteriormente. Mas as chamadas DLL são apenas chamadas de função, mesmo que sejam invólucros em torno de syscall
instruções.
Curiosidade: não Windows, ou seja, a convenção de chamada x86-64 System V (por exemplo, no Linux) não usa espaço de sombra e usa até 6 argumentos de registro de inteiro / ponteiro e até 8 argumentos de FP em registros XMM .
Usando a invoke
diretiva MASM (que conhece a convenção de chamada), você pode usar um ifdef para fazer uma versão deste que pode ser construída como 32 bits ou 64 bits.
ifdef rax
extrn MessageBoxA: PROC
extrn ExitProcess: PROC
else
.386
.model flat, stdcall
include kernel32.inc
includelib kernel32.lib
include user32.inc
includelib user32.lib
endif
.data
caption db 'WinAPI', 0
text db 'Hello World', 0
.code
main proc
invoke MessageBoxA, 0, offset text, offset caption, 0
invoke ExitProcess, eax
main endp
end
A variante macro é a mesma para ambos, mas você não aprenderá a montagem dessa maneira. Em vez disso, você aprenderá asm no estilo C. invoke
é para stdcall
ou fastcall
enquanto cinvoke
é para cdecl
ou argumento variávelfastcall
. O montador sabe qual usar.
Você pode desmontar a saída para ver como invoke
expandida.
title
nome do rótulo, encontro erros. No entanto, quando uso outra coisa como nome de rótulo mytitle
, tudo funciona bem.
Flat Assembler não precisa de um vinculador extra. Isso torna a programação em assembler bastante fácil. Também está disponível para Linux.
Isso é hello.asm
dos exemplos Fasm:
include 'win32ax.inc'
.code
start:
invoke MessageBox,HWND_DESKTOP,"Hi! I'm the example program!",invoke GetCommandLine,MB_OK
invoke ExitProcess,0
.end start
Fasm cria um executável:
> fasm hello.asm flat assembler versão 1.70.03 (memória de 1048575 kilobytes) 4 passagens, 1536 bytes.
E este é o programa do IDA :
Você pode ver as três chamadas: GetCommandLine
, MessageBox
e ExitProcess
.
Para obter um .exe com o compilador NASM e o vinculador do Visual Studio, este código funciona bem:
global WinMain
extern ExitProcess ; external functions in system libraries
extern MessageBoxA
section .data
title: db 'Win64', 0
msg: db 'Hello world!', 0
section .text
WinMain:
sub rsp, 28h
mov rcx, 0 ; hWnd = HWND_DESKTOP
lea rdx,[msg] ; LPCSTR lpText
lea r8,[title] ; LPCSTR lpCaption
mov r9d, 0 ; uType = MB_OK
call MessageBoxA
add rsp, 28h
mov ecx,eax
call ExitProcess
hlt ; never here
Se este código for salvo em, por exemplo, "test64.asm", então para compilar:
nasm -f win64 test64.asm
Produz "test64.obj" Em seguida, para vincular a partir do prompt de comando:
path_to_link\link.exe test64.obj /subsystem:windows /entry:WinMain /libpath:path_to_libs /nodefaultlib kernel32.lib user32.lib /largeaddressaware:no
onde path_to_link pode ser C: \ Arquivos de programas (x86) \ Microsoft Visual Studio 10.0 \ VC \ bin ou onde quer que esteja o programa link.exe em sua máquina, path_to_libs pode ser C: \ Arquivos de programas (x86) \ Windows Kits \ 8.1 \ Lib \ winv6.3 \ um \ x64 ou onde quer que estejam suas bibliotecas (neste caso, kernel32.lib e user32.lib estão no mesmo lugar, caso contrário, use uma opção para cada caminho que você precisa) e o / largeaddressaware: nenhuma opção é necessário para evitar a reclamação do linker sobre endereços longos (para user32.lib neste caso). Além disso, como é feito aqui, se o vinculador do Visual for invocado no prompt de comando, é necessário configurar o ambiente previamente (execute vcvarsall.bat e / ou consulte MS C ++ 2010 e mspdb100.dll).
default rel
no topo do seu arquivo para que os modos de endereçamento ( [msg]
e [title]
) usem o endereçamento relativo ao RIP em vez do absoluto de 32 bits.
A menos que você chame alguma função, isso não é nada trivial. (E, falando sério, não há diferença real na complexidade entre chamar printf e chamar uma função de API win32.)
Mesmo o DOS int 21h é apenas uma chamada de função, mesmo que seja uma API diferente.
Se quiser fazer isso sem ajuda, você precisa falar diretamente com o hardware de vídeo, provavelmente escrevendo bitmaps das letras "Olá, mundo" em um framebuffer. Mesmo assim, a placa de vídeo está fazendo o trabalho de traduzir esses valores de memória em sinais VGA / DVI.
Observe que, na verdade, nenhuma dessas coisas até o hardware é mais interessante no ASM do que em C. Um programa "hello world" se resume a uma chamada de função. Uma coisa boa sobre o ASM é que você pode usar qualquer ABI que desejar com bastante facilidade; você só precisa saber o que é essa ABI.
Os melhores exemplos são aqueles com fasm, porque fasm não usa um vinculador, o que esconde a complexidade da programação do Windows por outra camada opaca de complexidade. Se você está satisfeito com um programa que grava em uma janela gui, então há um exemplo para isso no diretório de exemplo do fasm.
Se você quiser um programa de console, que permite o redirecionamento da entrada padrão e da saída padrão também é possível. Há um programa de exemplo (altamente não trivial) disponível que não usa uma interface do usuário e funciona estritamente com o console, que é o próprio fasm. Isso pode ser reduzido ao essencial. (Eu escrevi um quarto compilador que é outro exemplo não-gui, mas também não é trivial).
Esse programa possui o seguinte comando para gerar um cabeçalho apropriado para o executável de 32 bits, normalmente feito por um vinculador.
FORMAT PE CONSOLE
Uma seção chamada '.idata' contém uma tabela que ajuda o Windows durante a inicialização a acoplar nomes de funções aos endereços de tempo de execução. Ele também contém uma referência a KERNEL.DLL, que é o sistema operacional Windows.
section '.idata' import data readable writeable
dd 0,0,0,rva kernel_name,rva kernel_table
dd 0,0,0,0,0
kernel_table:
_ExitProcess@4 DD rva _ExitProcess
CreateFile DD rva _CreateFileA
...
...
_GetStdHandle@4 DD rva _GetStdHandle
DD 0
O formato da tabela é imposto pelo windows e contém nomes que são procurados em arquivos de sistema, quando o programa é iniciado. O FASM esconde parte da complexidade por trás da palavra-chave rva. Portanto, _ExitProcess @ 4 é um rótulo fasm e _exitProcess é uma string pesquisada pelo Windows.
Seu programa está na seção '.text'. Se você declarar essa seção legível, gravável e executável, é a única seção que você precisa adicionar.
section '.text' code executable readable writable
Você pode chamar todas as instalações declaradas na seção .idata. Para um programa de console, você precisa de _GetStdHandle para encontrar os escritores de arquivos para entrada e saída padrão (usando nomes simbólicos como STD_INPUT_HANDLE que fasm encontra no arquivo de inclusão win32a.inc). Depois de ter os descritores de arquivo, você pode fazer WriteFile e ReadFile. Todas as funções são descritas na documentação do kernel32. Você provavelmente está ciente disso ou não tentaria programar em assembler.
Resumindo: existe uma tabela com nomes asci que se acoplam ao sistema operacional Windows. Durante a inicialização, isso é transformado em uma tabela de endereços chamáveis, que você usa em seu programa.
Se você quiser usar o NASM e o vinculador do Visual Studio (link.exe) com o exemplo Hello World de anderstornvig, será necessário vincular manualmente a C Runtime Libary que contém a função printf ().
nasm -fwin32 helloworld.asm
link.exe helloworld.obj libcmt.lib
Espero que isso ajude alguém.