Написать программу, выводящую строку "Hello World" на экран

Что-то совсем мало сказано про использование стандартной библиотеки Си.

Этот пост я направляю в первую очередь преподавателям и учителям.

Начинать HelloWorld с реализации для DOS, с использованием WinAPI или макросов, по моему мнению, очень не правильно.

Если изучать Ассемблер после Си, то можно легче понять происходящее, а самое главное, объяснить всем студентам/школьникам значимость Ассемблера, архитектуру компьютера и правила исполнения программ. Современный курс не должен концентрироваться на 100500 способах вывода последовательности байт в консоль и изучении систем исчисления. Чем быстрее будет изучен математический сопроцессор, SSE и прочие вкусности, тем больше можно успеть в рамках курса. А это значит, что надо пользоваться готовыми инструментами для взаимодействия человека и компьютера.

Win32

Теперь вспомним, что в Си программа начинает исполняться с функции

main

. Реализуем её. Эта функция возвращает число с типом

int

, для Win32, в соответствии с "соглашением о вызове"

cdecl

, оно должно быть возвращено через регистр

EAX

.

Процесс сборки готовой программы разобьём на два этапа:

- компиляция asm-файла в объектный файл,

- компоновка объектного файла со стандартными библиотеками средством, предполагающим сборку Си-приложения.

Первый пункт требует, что бы мы не забыли "опубликовать" нашу функцию. Второй - предполагает, что компоновщик будет ждать функцию

main

.

В качестве компилятора языка Ассемблера я буду использовать NASM, поменять его на другой не составит труда. В роли компоновщика рассмотрю MinGW и MSVS.

Первый пример: "Ничего не делаем"

AssemblerВыделить код

1
2
3
4
5
6

global _main
 
section .code
_main:
        mov     eax, 13
        ret

Компилируем, компонуем (MinGW), запускаем и смотрим код ошибки

BashВыделить код

1
2
3
4
5
6
7
8

D:work>nasm -f win32 hello.asm -o hello.obj
 
D:work>mingw32-gcc hello.obj -o hello.exe
 
D:work>hello.exe
 
D:work>echo %ERRORLEVEL%
13

Добавим использование функции

printf

. Не забываем объявить о ней компилятору Ассемблера:

AssemblerВыделить код

1
2
3
4
5
6
7
8
9
10
11
12
13
14

global _main
extern _printf
 
section .code
_main:
        push    msg
        call    _printf
        add     esp, 4
 
        mov     eax, 0
        ret
 
section .data
msg     db "Hello World", 10, 0

Компилируем, компонуем, запускаем и видим долгожданную строку.

Я говорил, что можно скомпоновать и с помощью MSVS:

BashВыделить код

| | |
| --- | --- |
| 1 | D:work>link hello.obj /out:hello.exe /subsystem:console /defaultlib:msvcrt |

Теперь вспомним, что в

main

передаются ещё и параметры. Они лежат в стеке. Там сейчас:

- адрес возврата (4 байта),

-

int argc

(4 байта),

-

char *argv[]

(4 байта).

AssemblerВыделить код

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

global _main
extern _printf
 
section .code
_main:
        mov     eax, [esp+8]
        mov     eax, [eax]
        push    eax
        push    msg
        call    _printf
        add     esp, 8 ; Не забываем, что теперь надо убрать 8 байт из стека
 
        mov     eax, 0
        ret
 
section .data
msg     db "Hello World!!!", 10
        db "My name is '%s'", 10, 0

Linux

А тут ничего почти не изменилось

. Мы не использовали системнозависимых функций. Но нам потребуется:

- избавиться от префикса "_",

- изменить формат объектного файла

AssemblerВыделить код

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

global main
extern printf
 
section .code
main:
        mov     eax, [esp+8]
        mov     eax, [eax]
        push    eax
        push    msg
        call    printf
        add     esp, 8
 
        mov     eax, 0
        ret
 
section .data
msg     db "Hello World!!!", 10
        db "My name is '%s'", 10, 0

Компилируем, компонуем, запускаем:

BashВыделить код

1
2
3
4
5

~/work$ nasm -f elf32 hello.asm -o hello.o
~/work$ gcc hello.o -o hello
~/work$ ./hello
Hello World!!!
My name is './hello'

Замечание:

Если добавить ключ "--prefix _" в nasm при компиляции в Win32, то последний код, будет работоспособным.

Win64

Тут нас ждут следующие изменения:

- Регистры увеличились и в размера и в количестве

.

- Параметры передаются через регистры, для нас важно, что целые числа и указатели будут лежать в RCX, RDX, R8 и R9, действительные в XMM0L, XMM1L, XMM2L и XMM3L, остальное через стек.

- Вызывающая подпрограмма должна зарезервировать в стеке место для 4-х регистров!!!

- Префикс не нужен.

- Изменился формат объектных файлов.

Остальное читаем в

http://msdn.microsoft.com/en-u... 35286.aspx

AssemblerВыделить код

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

global main
extern printf
 
section .code
main:
        sub     rsp, 48
 
        mov     rdx, [rdx]
        mov     rcx, msg
        call    printf
 
        add     rsp, 48
        mov     rax, 0
        ret
 
section .data
msg     db "Hello World!!!", 10
        db "My name is '%s'", 10, 0

Компиляция и компоновка (MinGW64):

BashВыделить код

1
2

D:Work>nasm -f win64 hello.asm -o hello.obj
D:Work>x86_64-w64-mingw32-gcc hello.obj -o hello.exe

Linux

Изменения:

- Параметры передаются через регистры. Целые числа/указатели в rdi, rsi, rdx, rcx, r8 и r9. Действительные xmm0-xxm7, остальное через стек.

- Если вызывается подпрограмма с переменным числом параметров, то в RAX указываем число использованных xmm-регистров!!!

- Префикс не нужен.

- Изменился формат объектных файлов.

AssemblerВыделить код

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

global main
extern printf
 
section .code
main:
        mov     rax, 0
        mov     rsi, [rsi]
        mov     rdi, msg
        call    printf
 
        mov     rax, 23
        ret
 
section .data
msg     db "Hello World!!!", 10
        db "My name is '%s'", 10, 0

Компиляция, компоновка:

BashВыделить код

1
2

~/work$ nasm -f elf64 hello.asm -o hello.o
~/work$ gcc hello.o -o hello

P.S.

AssemblerВыделить код

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

global main
extern wprintf
extern _setmode
 
section .code
main:
        push    0x20000 ; _O_U16TEXT
        push    1       ; stdout
        call    _setmode
        add     esp, 8
 
        push    msg
        call    wprintf
        add     esp, 4
        
        mov     eax, 0
        ret
 
section .data
msg     dw __utf16__(Hello World!!! u263a), 10
        dw __utf16__(Привет мир!!!), 10
        dw __utf16__(Hello, u4e16u754c!!!), 10, 0

Что бы увидеть последние два символа, запустите программу из ConEmu