Архитектура x86-64 под скальпелем ассемблерщика

Архитектура x86-64 под скальпелем ассемблерщика

крис касперски ака мыщъх no-email

32-битная эпоха уходит в прошлое, сдаваясь под натиском новых идей и платформ. оба флагмана рынка (Intel и AMD) представили 64-битные архитектуры, открывающие дверь в мир больших скоростей и производительных ЦП. это настоящий прорыв — новые регистры, новые режимы работы… попробуем с ним разобраться? в этой статье мы рассмотрим архитектуру AMD64 (она же x86-64) и покажем как с ней бороться.

Введение

64-битный лейбл звучит возбуждающе, но в практическом плане это всего лишь хитрый маркетинговый трюк, скрывающий не только достоинства, но и недостатки. Нам дарованы 64-битные операнды и 64-битная адресация. Казалось бы, лишние разряды карман не тянут и если не пригодятся, то по крайней мере не помешают. Так ведь нет! С ростом разрядности увеличивается и длина машинных команд, а, значит, время их загрузки/декодирования и размеры программы, поэтому для достижения не худшей производительности 64-битный процессор должен иметь более быструю память и более емкий кэш. Это раз.

 

Рисунок 1 64-разрядный лейбл, для разнообразия на китайском

64-битные целочисленные операнды становятся юзабельны только при обработке чисел порядка 233+ (8.589.934.592) и выше. Там, где 32-битному процессору требуется несколько тактов, 64-битный справляется за один. Но где вы видели такие числа в домашних и офисных приложениях? Не зря же инженеры из Intel пошли на сокращение разрядности АЛУ (арифметичного-логичесокго устройства), "ширина" которого в Pentium-4 составляет всего 16 бит, против 32-бит в Pentium-III. Это не значит, что Pentium-4 не может обрабатывать 32-разрядные числа. Может. Только он тратит на них больше времени, чем Pentium-III. Но, поскольку, процент подлинно 32-разрядных чисел (т. е. таких, что используют свыше 16 бит) в домашних приложениях относительно невысок, производительность падает незначительно. Зато ядро содержит меньше транзисторов, выделяет меньше тепла и лучше работает на повышенной тактовой частоте, т. е. в целом эффект положительный.

64-битная разрядность… Помилуйте! Адресовать 18.446.744.073.709.551.616 байт памяти не нужно даже Microsoft'у со вмести графическими заворотами! Из 4 Гбайт адресного пространства Windows Processional и Windows Server только 2 Гбайта выделяют приложениям. 3 Гбайта выделяет лишь Windows Advanced Server, и не потому, что больше выделить невозможно! x86 процессоры с легкостью адресуют вплоть до 16 Гбайт (по 4 Гбайта на код, данные, стек и кучу), опять-таки обходясь минимальной перестройкой операционной системы! Почему же до сих пор это не было сделано? Почему мы как лохи сидим на "жалких" 4 Гбайтах из которых реально доступны только два?! Да потому, что больше никому не нужно! Систему, адресующую 16 Гбайт, просто так не продашь, кого эти гигабайты интересуют? Вот "64-бита" совсем другое дело! Это освежает! Вот все вокруг них и танцуют.

Сравнивать 32- и 64-битные процессоры бессмысленно! Если 64-битный процессор на "домашнем" приложении оказывается быстрее, то отнюдь не за счет своей 64-битности, а благодаря совершенно независимым от нее конструктивным ухищрениям, на которых инженеры едва не разорвали себе задницы!

Впрочем, не будем о грустном. 64-бита все равно войдут в нашу жизнь. Для некоторых задач они очень даже ничего. Вот, например, криптография. 64-бита это же 8 байт! 8-символьные пароли можно полностью уместить в один регистр, не обращаясь к памяти, что дает невероятный результат! Скорость перебора увеличивается чуть ли не на порядок! Ну так чего же мы ждем?! Вперед! На штурм 64-битных вершин!

 

Рисунок 2 AMD Athlon 64 во всей своей красе

Что нам понадобиться?

  Рисунок 3 реакция 64-битного Линуха, запущенного под стандартной сборкой BOCHS'а ("your CPU does not support long…

Обзор x86-64

Наконец-то AMD сжалилась над нами и подарила программистам то, что все так долго ждали. К семи регистрам общего назначения (восьми — с учетом ESP)… Старые регистры, расширенные до 64-бит, получили имена RAX, RBX, RCX, RDX,…  

NOP ; команда, чем опкод мы хотим загрузить в AL

Это же умом поехать можно пока все это писать! И еще здесь очень легко ошибиться в размере команд, которых приходится либо вычислять вручную, либо… А теперь перепершем тот же самый пример на x86-64:  

NOP ; команда, чем опкод мы хотим загрузить в AL

Крррасота! Только следует помнить, что RIP всегда указывает на следующую, а отнюдь не текущую инструкцию! К сожалению, ни Jx RIP, ни CALL RIP не…   dec byte ptr [666h] ; уменьшить содержимое байта по адресу 666h на единицу

>>> врезка переход в 64-разрдяный режим

  //$FreeBSD: /repoman/r/ncvs/src/sys/boot/i386/libi386/amd64_tramp.S,v 1.4…  

Hello world на x86-64

В частности, вызов функции с пятью аргументами API_func(1,2,3,4,5) выглядит так:   mov dword ptr [rsp+20h], 5 ; кладем на стек пятый слева аргумент

Use64

 

xor r9,r9 ; обнуляем регистр r9

mov rax,r9 ; пересылаем в rax,r9 (можно сразу mov rax,0, но неинтересно)

ret ; выходим туда откуда пришли

Листинг 9 простейшая 64-битная программа

Никаких дополнительных аргументов командной строки указывать не надо, просто сказать "fasm file-name.asm" и все! Через несколько секунд образуется файл file-name.bin, который в hex-представлении выглядит следующим образом:

 

4D 31 C9 xor r9, r9

4C 89 C8 mov rax, r9

C3 retn

Листинг 10 дизассемблерный листинг простейшей 64-битной программы

Формально, это типичный com-файл, вот только запустить его не удастся (во всяком случае, ни одна популярная ось его не "съест") и необходимо замутить законченный ELF или PE, в заголовке которого будет явно прописана нужна разрядность.

Начиная с версии 1.64 ассемблер FASM поддерживает специальную директиву "format PE64", автоматически формирующую 64-разрядный PE-файл (директиву "use64" в этом случае указывать уже не нужно), а в каталоге EXAMPLES можно найти готовый пример PE64DEMO в котором показано как ее использовать на практике.

Ниже приведен пример x86-64 программы "hello, world" с комментариями:

 

; пример 64-битного PE файла

; для его выполнения необходимо иметь Windows XP 64-bit edition

 

; указываем формат

format PE64 GUI

 

; указываем точку входа

entry start

 

; создать кодовую секцию с атрибутами на чтение и исполнение

section '.code' code readable executable

start:

mov r9d,0 ; uType == MB_OK (кнопка по умолчанию)

; аргументы по соглашению x86-64

; передаются через регистры, не через стек!

; префикс d задает регистр размером в слово,

; можно использовать и mov r9,0, но тогда

; машинный код будет на байт длиннее

 

lea r8,[_caption] ; lpCaption передаем смещение

; команда lea занимает всего 7 байт,

; а mov reg, offset - целых 11, так что

; lea намного более предпочтительна

 

lea rdx,[_message] ; lpText передаем смещение выводимой строки

 

mov rcx,0 ; hWnd передам дескриптор окна-владельца

; (можно так же использовать xor rcx,rcx

; что на три байта короче)

 

call [MessageBox] ; вызываем функцию MessageBox

 

mov ecx,eax ; заносим в ecx результат возврата

; (Функция ExitProcess ожидает 32-битный параметр

; можно использовать и mov rcx,rax, но это будет

; на байт длиннее)

 

call [ExitProcess] ; вызываем функцию ExitProcess

 

; создать секцию данных с атрибутами на чтение и запись

; (вообще-то в данном случае атрибут на запись необязателен,

; поскольку мы ничего не пишем, а только читаем)

section '.data' data readable writeable

 

_caption db 'PENUMBRA is awesome!',0 ; ASCIIZ-строка заголовка окна

_message db 'Hello World!',0 ; ASCIIZ-строка выводимая на экран

 

; создать секцию импорта с атрибутами на чтение и запись

; (здесь атрибут на запись обязателен, поскольку при загрузке PE-Файла

; в секцию импорта ; будут записываться фактические адреса API-функций)

section '.idata' import data readable writeable

 

dd 0,0,0,RVA kernel_name,RVA kernel_table

dd 0,0,0,RVA user_name,RVA user_table

dd 0,0,0,0,0 ; завершаем список двумя 64-разряными нулеми!!!

 

kernel_table:

ExitProcess dq RVA _ExitProcess

dq 0 ; завершаем список 64-разряным нулем!!!

 

user_table:

MessageBox dq RVA _MessageBoxA

dq 0

 

kernel_name db 'KERNEL32.DLL',0

user_name db 'USER32.DLL',0

 

_ExitProcess dw 0

db 'ExitProcess',0

_MessageBoxA dw 0

db 'MessageBoxA',0

Листинг 11 64-битное приложение "hello, world" под Windows на FASM'е

 

Рисунок 8 64-битный файл — первый полет

Ассемблируем файл (fasm PE64DEMO.ASM) и запустим образовавшийся EXE на выполнение. Под 32-разрядной Windows он, естественно, не запустится и она скажет "мяу":

 

Рисунок 9 реакция 32-битной Windows на попытку запуска 64-битного PE-файла

Вдоволь наигравшись нашем первым x86-64 файлом, загрузим его в дизассемблер (например, в IDA Pro 4.7. Она хоть и материться, предлагая использовать специальную 64-битную версию, но при нажатии на "yes" все конкретно дизассемблирует, во всяком случае до тех пор пока не столкнется с подлинным 64-битным адресом или операндом, с которым произойдет обрезание, в частности mov r9,1234567890h дизассемблируется как mov r9, 34567890h, так что переход на 64-битную версию IDA все же очень желателен, тем более, что начиная с IDA 4.9 она входит в базовую поставку). Посмотрим, что у него внутри?

А внутри у него вот что:

 

.code:0000000000401000 41 B9 00 00 00 00 mov r9d, 0

.code:0000000000401006 4C 8D 05 F3 0F 00 00 lea r8, aPENUMBRA

.code:000000000040100D 48 8D 15 03 10 00 00 lea rdx, aHelloWorld ; "Hello World!"

.code:0000000000401014 48 C7 C1 00 00 00 00 mov rcx, 0

.code:000000000040101B FF 15 2B 20 00 00 call cs:MessageBoxA

.code:0000000000401021 89 C1 mov ecx, eax

.code:0000000000401023 FF 15 13 20 00 00 call cs:ExitProcess

Листинг 12 дизассемблерный листинг 64-битного приложения "hello, world!"

Что ж… довольно громоздко, объемно и концептуально. Для сравнения, дизассемблированный листинг аналогичного 32-разрядного файла приведен ниже. Старый x86 код в 1,6 раз короче! А ведь это только демонстрационная программа из нескольких строк! На полновесных приложениях разрыв будет только нарастать! Так что не стоит злоупотреблять 64-разрядным кодом без необходимости. Его следует использовать только там, где 64-битная арифметика и 8 дополнительных регистров действительно дают ощутимый выигрыш. Например, в математических задачах или программах для вскрытия паролей.

 

Рисунок 10 дизассемблирование 64-битного PE-файла 32-битной версий IDA Pro

code:00401000 6A 00 push 0

code:00401002 68 00 20 40 00 push offset aPENUMBRA

code:00401007 68 17 20 40 00 push offset aHelloWorld

code:0040100C 6A 00 push 0

code:0040100E FF 15 44 30 40 00 call ds:MessageBoxA

code:00401014 6A 00 push 0

code:00401016 FF 15 3C 30 40 00 call ds:ExitProcess

Листинг 13 дизассемблерный листинг 32-битного приложения "hello, world!"

В качестве заключительно упражнения перепишем наше приложение в стиле MASM, поклонников которого нужно не бить, а уважать (как ни крути, а все-таки патриарх). Никаких радикальных отличий не наблюдается:

 

; объявляем внешние API-функции, которые мы будем вызывать

extrn MessageBoxA: PROC

extrn ExitProcess: PROC

 

; секция данных с атрибутами по умолчанию (чтение и запись)

.data

mytit db 'PENUMBRA is awesome!', 0

mymsg db 'Hello World!', 0

 

; секция кода с атрибутами по умолчанию (чтение и исполнение)

.code

Main:

mov r9d, 0 ; uType = MB_OK

lea r8, mytit ; LPCSTR lpCaption

lea rdx, mymsg ; LPCSTR lpText

mov rcx, 0 ; hWnd = HWND_DESKTOP

call MessageBoxA

mov ecx, eax ; uExitCode = MessageBox(...)

call ExitProcess

End Main

Листинг 14 64-битное приложение "hello, world" под Windows на MASM'е

Ассемблирование и линковка проходит так: "ml64 XXX.asm /link /subsystem:windows /defaultlib:kernel32.lib /defaultlib:user32.lib /entry:main" в результате чего образуется готовый к употреблению exe-файл с румяной поджаренной корочкой нашего ЦП (FASM ассемблирует намного быстрее).

Примеры более сложных программ легко найти в сети. Как показывает практика, запросы типа "x86-64 [AMD64] assembler example" катастрофически неэффективны и гораздо лучше использовать "mov rax" (без кавычек) или вроде того.

Заключение

Вот мы и познакомились с архитектурой x86-64! Здесь действительно есть место где развернутся и чему поучиться! Насколько эти знания окажутся востребованы на практике — так сразу и не скажешь. У AMD есть хорошие шансы пошатнуть рынок, но ведь и Intel не дремлет, активно продвигая собственные 64-разрядные платформы, известные под общем именем IA64, но о них как ни будь в другой раз…