Язык ассемблера и исполняемые файлы¶
Ассемблер¶
Писать на АСМе сейчас практически не нужно. Но ВСЕГДА рано или поздно сталкивался с тем, что нужно уметь читать АСМ. Либо для того, чтобы сделать действительно оптимальный код, либо — что гораздо чаще — чтобы суметь правильно прочитать stack trace и поймать эту самую чертову ошибку. Мне довольно часто приходилось видеть этот "кошмар прикладного программиста", когда большая и сложная программа валится на невнятном сообщении "выполнена недопустимая операция". И без знания АСМа понять, что случилось, невозможно.
Языки ассемблеров были первыми "высокоуровневыми" языками по сравнению с программированием непосредственно в адресных кодах. В них появились:
- мнемонические инструкции (
mov,push, ...) вместо кодов операций - метки и арифметика адресов в дополнение к абсолютным адресам в памяти
- директивы
- макросы
- подпрограммы
55
89 E5
83 EC 08
C7 45 FC 01 00 00 00
83 EC 0C
6A 00
E8 D1 FE FF FF
push %ebp
mov %esp, %ebp
sub $0x8, %esp
movl $0x1, -4(%ebp)
sub $0xc, %esp
push $0x0
call 8048348
int main()
{
int i = 1;
exit(0);
}
Еще один пример — классический Hello World
#include <stdio.h>
int main()
{
printf("hello, world");
return 0;
};
преобразованный в ассемблер компилятором MSVC (cl 1.cpp /Fa1.asm):
CONST SEGMENT
$SG3830 DB 'hello, world', 00H
CONST ENDS
PUBLIC _main
EXTRN _printf:PROC
; Function compile flags: /Odtp
_TEXT SEGMENT
_main PROC
; File c:\...\1.cpp
; Line 4
push ebp
mov ebp, esp
; Line 5
push OFFSET $SG3830
call _printf
add esp, 4
; Line 6
xor eax, eax
; Line 7
pop ebp
ret 0
_main ENDP
_TEXT ENDS
и компилятором GCC:
.file “ctest.c”
.version “01.01”
gcc2_compiled.:
.section .rodata
.LC0:
.string “Hello, world!\n”
.text
.align 16
.globl main
.type main,@function
main:
pushl %ebp
movl %esp, %ebp
subl $8, %esp
subl $12, %esp
pushl $.LC0
call printf
addl $16, %esp
subl $12, %esp
pushl $0
call exit
.Lfe1:
.size main,.Lfe1-main
.ident “GCC: (GNU) 2.96 20000731 (Linux-Mandrake 8.0 2.96-0.48mdk)”
Соглашения о вызовах¶
Это частично определяемый аппаратной архитектурой и частично конкретной средой исполнения механизм вызова подпрограмм. Соглашения включают ответы на вопросы: как передаются/возвращаются аргументы, как распределяется работа при вызове/завершении подпрограммы.
Конкретные примеры реализации:
- x86: гибридная
- PowerPC: в регистрах
- MIPS: первые 4 параметра – в регистрах $a0-$a3; возвращаемое значение – в $v0
- SPARC: 8 in-, 8 out-регистров
- System V ABI in-регистры %i0-%i5, %i6 (EIP), %i7 (возвращаемое значение).
- Threaded code: на стэке, вся работа на вызываемую подпрограмму
- ARM: r15 (счетчик), r13 (стэк), r0-r3 (in-out)
push eAX ; последний аргумент — содержимое регистра eAX push byte[eBP+20] ; предпоследний аргумент — содержимое ячейки памяти по адресу = адрес в eBP + 20 байт push 3 ; первый аргумент — константа 3 call calc ; вызов подпрограммы calc, возвращаемое значение будет в eAXТипичная структура подпрограммы:
calc: push eBP ; сохранить указатель на стек mov eBP,eSP ; получить новый указатель на стек sub eSP,4*3 ; выделить место для локальных переменных ... ; произвести вычисления, в конце концов результат записать в eAX mov eSP,eBP ; "освободить" место, занимаемое локальными переменными pop eBP ; восстановить указатель на стек ret 0 ; вернуться в место вызова
Литература:¶
- Ассемблер в Linux для программистов C
- Ассемблеры для Linux: Сравнение GAS и NASM
- Введение в Reverse Engineering