Como escrever hello world em assembler no Windows?

96

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?

feiroox
fonte
3
Confira também o kit inicial de montagem de janelas Small Is Beautiful de Steve Gibson .
Jeremy,
Não usar c-libraries é uma restrição um tanto estranha. É necessário chamar alguma biblioteca dentro do sistema operacional MS-Windows. provavelmente kernel32.dll. Se a Microsoft escreveu isso em c ou Pascal, parece irrelevante. Isso significa que apenas funções fornecidas pelo sistema operacional podem ser chamadas, o que em um sistema do tipo Unix seria chamado de chamadas de sistema?
Albert van der Horst de
Com as bibliotecas C, presumo que ele quer dizer sem usar bibliotecas de tempo de execução C como as que vêm com o GCC ou MSVC. Claro que ele terá que usar algumas DLLs padrão do Windows, como kernel32.dll.
Rudy Velthuis
2
A distinção entre kernel32.dll e uma biblioteca de tempo de execução gcc não está no formato (ambos são dll) e não está no idioma (ambos são provavelmente c, mas isso está oculto). A diferença é entre fornecido pelo sistema operacional ou não.
Albert van der Horst
Eu estive procurando por isso também lol não consegui encontrar nada com fasm sem includes
bluejayke

Respostas:

39

Exemplos de NASM .

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 .comexecutável para que seja carregado cs:100hcom todos os registradores de segmento iguais uns aos outros (minúsculo modelo de memória).

Boa sorte.

Peter Cordes
fonte
29
A pergunta menciona explicitamente "sem usar bibliotecas C"
mmx
25
Errado. A própria biblioteca C obviamente pode, então é possível. É apenas um pouco mais difícil, na verdade. Você só precisa chamar WriteConsole () com os 5 parâmetros corretos.
MSalters em
12
Embora o segundo exemplo não chame nenhuma função de biblioteca C, também não é um programa do Windows. A máquina virtual DOS será acionada para executá-lo.
Rômulo Ceccon
7
@Alex Hart, seu segundo exemplo é para DOS, não para Windows. No DOS, os programas no modo minúsculo (arquivos .COM, sob código total de 64Kb + dados + pilha) começam em 0x100h porque os primeiros 256 bytes no segmento são obtidos pelo PSP (argumentos de linha de comando etc.). Veja este link: en.wikipedia.org/wiki/Program_Segment_Prefix
zvolkov
7
Não foi isso que foi pedido. O primeiro exemplo usa a biblioteca C e o segundo é MS-DOS, não Windows.
Paulo Pinto
133

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 
amigo
fonte
21
você provavelmente precisará incluir o kernel32.lib para vincular isso (eu fiz). link / subsistema: console / nodefaultlib / entrada: main hello.obj kernel32.lib
Zach Burlingame
6
Como vincular o obj com ld.exe do MinGW?
DarrenVortex
4
@DarrenVortexgcc hello.obj
turvo
4
Isso também funcionaria usando linkers gratuitos como Alink de sourceforge.net/projects/alink ou GoLink de godevtool.com/#linker ? Não quero instalar o Visual Studio apenas para isso?
jj_
22

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.

Win32 MASM

;---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

Win64 MASM

;---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 . ( mainO 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 syscallinstruçõ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 invokediretiva 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 stdcallou fastcallenquanto cinvokeé para cdeclou argumento variávelfastcall . O montador sabe qual usar.

Você pode desmontar a saída para ver como invokeexpandida.

PhiS
fonte
1
1 para sua resposta. Você pode adicionar o código assembly para Windows no ARM (WOA) também?
Annie
2
Por que o rsp requer 0x28 bytes e não 0x20? Todas as referências na convenção de chamada dizem que deveria ser 32, mas parece exigir 40 na prática.
douggard
1
No código da sua caixa de mensagem de 32 bits, por algum motivo, quando uso o titlenome do rótulo, encontro erros. No entanto, quando uso outra coisa como nome de rótulo mytitle, tudo funciona bem.
user3405291
como fazer sem inclui?
bluejayke
13

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.asmdos 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 :

insira a descrição da imagem aqui

Você pode ver as três chamadas: GetCommandLine, MessageBoxe ExitProcess.

ceving
fonte
isso usa um include e uma GUI. Como fazemos isso apenas para o CMD, sem nenhum includes?
bluejayke
você pode me apontar para uma seção que grava no console sem qualquer dll?
bluejayke
12

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).

rarias
fonte
2
Eu recomendo fortemente usar default relno 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.
Peter Cordes
Obrigado por explicar como fazer o link! Você salvou minha saúde mental. Eu estava começando a puxar meu cabelo para fora sobre 'erro LNK2001: símbolo externo não resolvido ExitProcess' e erros semelhantes ...
Nik
7

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.

Capitão Segfault
fonte
Este é um ponto excelente --- ASM e C dependem de uma função fornecida pelo sistema operacional (_WriteFile no Windows). Então, onde está a mágica? Ele está no código do driver de dispositivo da placa de vídeo.
Assad Ebrahim
2
Isso está completamente fora do ponto. O autor da postagem pergunta a um programa assembler que roda "no Windows". Isso significa que os recursos do Windows podem ser usados ​​(por exemplo, kernel32.dll), mas não outros recursos como o libc no Cygwin. Pelo amor de Deus, o pôster diz explicitamente sem c-bibliotecas.
Albert van der Horst
5

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.

Albert van der Horst
fonte
O FASM pode não usar um vinculador, mas ainda precisa montar um arquivo PE. O que significa que, na verdade, ele não apenas monta código, mas também assume uma tarefa que normalmente um vinculador executaria e, como tal, é, na minha humilde opinião, enganoso chamar a ausência de um vinculador de "ocultando complexidade", muito pelo contrário - a tarefa de um montador é montar um programa, mas deixe para o vinculador incorporar o programa em uma imagem de programa que pode depender de muitas coisas. Como tal, acho a separação entre um vinculador e um montador uma coisa boa , que parece que você discorda.
amn
@amn Pense desta forma. Se você usar um vinculador para criar o programa acima, ele fornece mais informações sobre o que o programa faz ou em que consiste? Se eu olhar para a fonte do fasm, conheço a estrutura completa do programa.
Albert van der Horst
Ponto justo. Por outro lado, separar a vinculação de todo o resto também tem seus benefícios. Normalmente, você tem acesso a um arquivo de objeto (o que permite que se inspecione a estrutura de um programa também, independentemente do formato do arquivo de imagem do programa), você pode invocar um linker diferente de sua preferência, com opções diferentes. É sobre reutilização e composibilidade. Com isso em mente, o FASM fazer tudo porque é "conveniente" quebra esses princípios. Não sou principalmente contra - vejo a justificativa para isso - mas eu, por exemplo, não preciso disso.
amn
obter erro por isntrução ilegal na linha superior em janelas fasm de 64 bits
bluejayke
@bluejayke Provavelmente você não tinha a documentação do fasm em mãos. FORMAT PE gera um executável de 32 bits, que uma janela de 64 bits se recusa a executar. Para um programa de 64 bits, você deseja FORMAT PE64. Certifique-se também de usar as instruções adequadas de 64 bits em seu programa.
Albert van der Horst,
3

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.

tboerman
fonte
O autor das perguntas quer saber como alguém escreveria o printf com base nas facilidades que o Windows oferece, então isso é totalmente fora de propósito.
Albert van der Horst,