Як написати hello world в асемблері під Windows?


94

Я хотів написати щось базове в збірці під Windows, я використовую NASM, але не можу нічого працювати.

Як писати та компілювати привіт світ без допомоги функцій C у Windows?


3
Також ознайомтесь із початковим набором збірки вікон Small Is Beautiful Windows Стіва Гібсона .
Джеремі,

Не використання c-бібліотек - це дещо дивне обмеження. Потрібно викликати якусь бібліотеку в операційній системі MS-Windows. ймовірно kernel32.dll. Якщо Microsoft писала це на c або Pascal, це здається неактуальним. Чи означає це, що можна викликати лише функції, що постачаються з ОС, що в системі типу Unix називатимуть системними викликами?
Альберт ван дер Горст,

З бібліотеками C я припускаю, що він або вона має на увазі без використання бібліотек середовища виконання, таких як ті, що постачаються з GCC або MSVC. Звичайно, йому або їй доведеться використовувати деякі стандартні бібліотеки DLL для Windows, такі як kernel32.dll.
Руді Велтуїс,

2
Різниця між kernel32.dll та бібліотекою середовища виконання gcc полягає не у форматі (обидва є dll) і не в мові (обидва, ймовірно, c, але це приховано.) Різниця полягає в тому, що постачається з ОС чи ні.
Альберт ван дер Горст

Я шукав це також хаха не міг знайти нічого з fasm без включає
bluejayke

Відповіді:


39

Приклади NASM .

Виклик libc stdio printf, реалізаціяint 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

Тоді біжи

nasm -fwin32 helloworld.asm
gcc helloworld.obj
a

Існує також «Безглуздий посібник для початківців для Hello World» в Назмі без використання бібліотеки C. Тоді код виглядав би так.

16-розрядний код із системними викликами MS-DOS: працює в емуляторах DOS або в 32-розрядної Windows із підтримкою NTVDM . Неможливо запустити "безпосередньо" (прозоро) під будь-якою 64-розрядною Windows, оскільки ядро ​​x86-64 не може використовувати режим vm86.

org 100h
mov dx,msg
mov ah,9
int 21h
mov ah,4Ch
int 21h
msg db 'Hello, World!',0Dh,0Ah,'$'

Побудуйте це у .comвиконуваний файл, щоб він завантажувався cs:100hз усіма регістрами сегментів, рівними один одному (крихітна модель пам'яті).

Удачі.


28
У запитанні прямо згадується "без використання бібліотек C"
Мехрдад Афшарі,

25
Неправильно. Сама бібліотека C, очевидно, може, тому це можливо. Насправді це лише трохи складніше. Вам просто потрібно викликати WriteConsole () з правильними 5 параметрами.
MSalters

12
Хоча другий приклад не викликає жодної функції бібліотеки C, це також не програма Windows. Віртуальна машина DOS буде запущена для її запуску.
Rômulo Ceccon

7
@Alex Hart, його другий приклад - для DOS, а не для Windows. У DOS програми в крихітному режимі (файли .COM, загальний код + дані + стек розміром 64 Кб) починаються з 0x100h, оскільки перші 256 байтів у сегменті беруться PSP (аргументи командного рядка тощо). Дивіться це посилання: en.wikipedia.org/wiki/Program_Segment_Prefix
zvolkov

7
Це не те, про що просили. Перший приклад використовує бібліотеку C, а другий - MS-DOS, а не Windows.
Паулу Пінто,

131

Цей приклад показує, як перейти безпосередньо до API Windows, а не посилатись у стандартній бібліотеці C.

    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:

Для компіляції вам знадобляться NASM та LINK.EXE (із Visual Studio Standard Edition)

   nasm -fwin32 привіт.asm
   посилання / підсистема: console / nodefaultlib / entry: main hello.obj 

21
вам, ймовірно, потрібно включити kernel32.lib, щоб зв’язати це (я це зробив). посилання / підсистема: console / nodefaultlib / entry: main hello.obj kernel32.lib
Zach Burlingame

5
Як зв'язати obj з ld.exe від MinGW?
DarrenVortex

4
@DarrenVortexgcc hello.obj
towry

4
Чи буде це також працювати з використанням безкоштовних посилань, таких як Alink з sourceforge.net/projects/alink або GoLink з godevtool.com/#linker ? Я не хочу встановлювати Visual Studio лише для цього?
jj_

21

Це приклади Win32 та Win64 із використанням викликів Windows API. Вони скоріше для MASM, ніж NASM, але подивіться на них. Більше деталей ви можете знайти в цій статті.

Тут використовується MessageBox замість друку на 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

Щоб зібрати та зв’язати їх за допомогою MASM, використовуйте це для 32-розрядних виконуваних файлів:

ml.exe [filename] /link /subsystem:windows 
/defaultlib:kernel32.lib /defaultlib:user32.lib /entry:Main

або це для 64-розрядних виконуваних файлів:

ml64.exe [filename] /link /subsystem:windows 
/defaultlib:kernel32.lib /defaultlib:user32.lib /entry:main

Чому ОС Windows x64 потрібно зарезервувати 28 год байтів простору стека перед a call? Це 32 байти (0x20) тіньового простору, відомого як домашній простір, як вимагає принцип виклику. І ще 8 байтів для повторного вирівнювання стека на 16, оскільки умова виклику вимагає, щоб RSP вирівнювалося до 16 байт перед a call. (Нашіmain зробив абонент (у коді запуску CRT). 8-байтова адреса повернення означає, що RSP знаходиться на 8 байт від 16-байтової межі при вході до функції.)

Тіньовий простір може використовуватися функцією для скидання своїх аргументів реєстру поруч із місцем розташування будь-яких аргументів стека (якщо такі є). A system callвимагає 30 годин (48 байт), щоб також зарезервувати місце для r10 та r11 на додаток до раніше згаданих 4 регістрів. Але виклики DLL - це лише виклики функцій, навіть якщо вони обгортковіsyscall інструкції.

Цікавий факт: не Windows, тобто умова викликів x86-64 System V (наприклад, на Linux) взагалі не використовує тіньовий простір і використовує до 6 аргументів цілочисельних / покажчикових регістрів та до 8 аргументів FP у регістрах XMM .


Використовуючи invokeдирективу MASM (яка знає принцип викликів), ви можете використовувати один ifdef, щоб зробити версію цього, яка може бути побудована як 32-розрядна або 64-розрядна.

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

Варіант макросу однаковий для обох, але ви не навчитеся складанню таким чином. Натомість ви дізнаєтесь про ASM у стилі C. invokeдля stdcallабо в fastcallтой час як cinvokeдля cdeclабо змінного аргументуfastcall . Асемблер знає, що використовувати.

Ви можете розібрати вихідні дані, щоб побачити, наскільки invokeрозширено.


1
+1 за вашу відповідь. Чи можете ви також додати код збірки для Windows на ARM (WOA)?
Енні

1
Чому rsp вимагає 0x28 байт, а не 0x20? Усі посилання на конвенцію про дзвінки говорять про те, що це має бути 32, але на практиці це вимагає 40.
douggard

У вашому 32-розрядному коді вікна повідомлення чомусь, коли я використовую titleяк ім'я мітки, я стикаюся з помилками. Однак коли я використовую щось інше як назву етикетки, як-от mytitle, все працює нормально.
user3405291

як це зробити, що включає?
bluejayke

14

Flat Assembler не потребує додаткового компонувача. Це робить програмування на асемблері досить простим. Він також доступний для Linux.

Це hello.asmз прикладів 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 створює виконуваний файл:

> fasm hello.asm
плоский асемблер версії 1.70.03 (1048575 кілобайт пам'яті)
4 проходи, 1536 байт.

І це програма в IDA :

введіть тут опис зображення

Ви можете побачити три виклики: GetCommandLine, MessageBoxі ExitProcess.


тут використовується включення та графічний інтерфейс, як ми можемо це зробити лише для CMD, не включаючи взагалі?
bluejayke

Спробували прочитати посібник? flatassembler.net/docs.php?article=manual#2.4.2
закінчується

Ви можете підказати мені розділ, який пише на консоль без жодних dll?
bluejayke

12

Щоб отримати .exe із компілятором NASM та компонувальником Visual Studio, цей код чудово працює:

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

Якщо цей код збережено, наприклад, "test64.asm", то для компіляції:

nasm -f win64 test64.asm

Виробляє "test64.obj" Потім посилання з командного рядка:

path_to_link\link.exe test64.obj /subsystem:windows /entry:WinMain  /libpath:path_to_libs /nodefaultlib kernel32.lib user32.lib /largeaddressaware:no

де path_to_link може бути C: \ Program Files (x86) \ Microsoft Visual Studio 10.0 \ VC \ bin або де б не була ваша програма link.exe на вашому комп'ютері, path_to_libs може бути C: \ Program Files (x86) \ Windows Kits \ 8.1 \ Lib \ winv6.3 \ um \ x64 або де б не були ваші бібліотеки (у цьому випадку і kernel32.lib, і user32.lib знаходяться в одному місці, інакше використовуйте один параметр для кожного потрібного вам шляху) та / largeaddressaware: жодного параметра необхідно, щоб уникнути скарг лінкера на довгі адреси (у цьому випадку для user32.lib). Крім того, як це робиться тут, якщо посилання Visual викликається з командного рядка, необхідно попередньо налаштувати середовище (запустити один раз vcvarsall.bat та / або див. MS C ++ 2010 та mspdb100.dll).


2
Я настійно рекомендую використовувати default relу верхній частині файлу, тому ці режими адресації ( [msg]і [title]) використовують RIP-відносну адресацію замість 32-бітової абсолютної.
Пітер Кордес,

Дякуємо за пояснення, як зв’язати! Ти врятував моє психічне здоров'я. Я починав виривати волосся за "помилку LNK2001: невирішений зовнішній символ ExitProcess" та подібні помилки ...
Нік,

5

Якщо ви не викликаєте якусь функцію, це зовсім не тривіально. (І, серйозно, немає реальної різниці в складності між викликом printf і викликом функції win32 api.)

Навіть DOS int 21h насправді є лише викликом функції, навіть якщо це інший API.

Якщо ви хочете зробити це без допомоги, вам потрібно поговорити безпосередньо з вашим обладнанням для відео, ймовірно, ввівши растрові зображення літер "Hello world" у буфер кадрів. Навіть тоді відеокарта виконує роботу з перетворення цих значень пам’яті у сигнали VGA / DVI.

Зауважте, що насправді ніщо з цього матеріалу аж до апаратного забезпечення не є цікавішим у ASM, ніж у C. Програма "привіт світ" зводиться до виклику функції. Одна приємна річ про ASM полягає в тому, що ви можете досить легко використовувати будь-який ABI; Вам просто потрібно знати, що це за ABI.


Це чудовий момент - як ASM, так і C покладаються на функцію, що надається ОС (_WriteFile в Windows). То де ж магія? Це в коді драйвера пристрою для відеокарти.
Асад Ебрагім

2
Це ґрунтовно, крім суті. Плакат запитує програму-асемблер, яка працює "під Windows". Це означає, що можна використовувати засоби Windows (наприклад, kernel32.dll), але не інші засоби, такі як libc під Cygwin. За плач уголос, плакат прямо говорить, що немає c-бібліотек.
Альберт ван дер Горст,

5

Найкращими прикладами є ті, що мають fasm, оскільки fasm не використовує компоновщик, який приховує складність програмування вікон за іншим непрозорим шаром складності. Якщо ви задоволені програмою, яка пише у вікно графічного інтерфейсу, тоді є приклад для цього у прикладі каталогу fasm.

Якщо вам потрібна консольна програма, яка дозволяє переспрямувати стандартний вхід і стандартний вихід, що також можливо. Доступна (насправді вкрай нетривіальна) прикладна програма, яка не використовує графічний інтерфейс і працює суворо з консоллю, тобто сама fasm. Це можна звести до найнеобхіднішого. (Я написав четвертий компілятор, який є ще одним не-графічним прикладом, але він також нетривіальний).

Така програма має наступну команду для створення належного заголовка для 32-розрядного виконуваного файлу, як правило, виконуваного лінкером.

FORMAT PE CONSOLE 

Розділ під назвою '.idata' містить таблицю, яка допомагає Windows під час запуску поєднувати імена функцій з адресами середовища виконання. Він також містить посилання на KERNEL.DLL, який є операційною системою 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

Формат таблиці накладається вікнами і містить імена, які шукаються у системних файлах під час запуску програми. FASM приховує деяку складність ключового слова rva. Отже, _ExitProcess @ 4 - це ярлик fasm, а _exitProcess - рядок, який шукає Windows.

Ваша програма знаходиться в розділі '.text'. Якщо ви оголосите цей розділ доступним для читання та виконанням, це єдиний розділ, який вам потрібно додати.

    section '.text' code executable readable writable

Ви можете зателефонувати у всі заклади, про які ви заявили в розділі .idata. Для консольної програми вам потрібно _GetStdHandle, щоб знайти подані ним дескриптори для стандартного входу та стандартного виходу (використовуючи символічні імена, такі як STD_INPUT_HANDLE, які fasm знаходить у файлі включення win32a.inc). Отримавши дескриптори файлів, ви можете зробити WriteFile та ReadFile. Усі функції описані в документації до ядра 32. Ви, мабуть, це знаєте, або не хотіли б спробувати програмування на асемблері.

Підсумовуючи: Існує таблиця з іменами asci, які поєднуються з ОС Windows. Під час запуску це перетворюється в таблицю адрес, що викликаються, які ви використовуєте у своїй програмі.


FASM може не використовувати компоновщик, але він все одно повинен зібрати файл PE. Що означає, що насправді він не просто збирає код, а й бере на себе роботу, яку зазвичай виконує лінкер, і, як така, на мою скромну думку, оманливо називати відсутність лінкера "приховуванням складності", а навпаки - робота асемблера полягає в тому, щоб зібрати програму, але залишити лінкеру вбудувати програму в образ програми, який може залежати від багатьох речей. Таким чином, я вважаю, що розмежування між компонувальником та асемблером - це добре , і, як видається, ви не погоджуєтесь.
амн

@amn Подумайте про це таким чином. Якщо ви використовуєте компоновщик для створення вищезазначеної програми, це дає вам більше розуміння того, що робить програма, або з чого вона складається? Якщо я подивлюсь на джерело fasm, то знаю повну структуру програми.
Альберт ван дер Горст,

Справедливий момент. З іншого боку, відокремлення посилання від усього іншого також має свої переваги. Зазвичай у вас є доступ до об'єктного файлу (що значно сприяє тому, що людина також може перевіряти структуру програми, незалежно від формату файлу зображення програми), ви можете викликати інший компонувальник за вашим уподобанням, з різними параметрами. Йдеться про багаторазове використання та композицію. Маючи це на увазі, FASM, роблячи все, тому що це "зручно" порушує ці принципи. Я принципово не проти - я бачу в цьому їх виправдання, - але мені, наприклад, це не потрібно.
ранку

отримати помилку за незаконне невикористання верхніх рядків у 64-розрядних вікнах
fasm

@bluejayke Можливо, у вас під рукою не було документації для FASM. FORMAT PE генерує 32-розрядний виконуваний файл, який 64-розрядне вікно відмовляється запускати. Для 64-розрядної програми вам потрібен FORMAT PE64. Також переконайтеся, що ви використовуєте належні 64-розрядні інструкції у своїй програмі.
Альберт ван дер Горст,

3

Якщо ви хочете використовувати NASM та зв’язувач Visual Studio (link.exe) з прикладом Hello World від anderstornvig, вам доведеться вручну пов’язувати бібліотеку середовища виконання C, що містить функцію printf ().

nasm -fwin32 helloworld.asm
link.exe helloworld.obj libcmt.lib

Сподіваюся, це комусь допомагає.


Афіша запитань хоче знати, як хтось писатиме printf на основі можливостей, які надає Windows, тож це знову ж таки абсолютно не до речі.
Альберт ван дер Горст,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.