Язык ассемблера и исполняемые файлы

Ассемблер

Писать на АСМе сейчас практически не нужно. Но ВСЕГДА рано или поздно сталкивался с тем, что нужно уметь читать АСМ. Либо для того, чтобы сделать действительно оптимальный код, либо — что гораздо чаще — чтобы суметь правильно прочитать stack trace и поймать эту самую чертову ошибку. Мне довольно часто приходилось видеть этот "кошмар прикладного программиста", когда большая и сложная программа валится на невнятном сообщении "выполнена недопустимая операция". И без знания АСМа понять, что случилось, невозможно.

Андрей Светлов

Языки ассемблеров были первыми "высокоуровневыми" языками по сравнению с программированием непосредственно в адресных кодах. В них появились:

  • мнемонические инструкции (mov, push, ...) вместо кодов операций
  • метки и арифметика адресов в дополнение к абсолютным адресам в памяти
  • директивы
  • макросы
  • подпрограммы
Пример эквивалентных программ в адресных кодах, на ассемблере (GNU Assembler) и на C:
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)
Типичная структура вызова подпрограммы на архитектуре x86:
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             ; вернуться в место вызова

Литература:

Форматы исполняемых файлов

Also available in: HTML TXT