Обращения к памяти
Сохранить значение регистра eax по адресу 0x40100, а потом загрузить обратно в регистр ebx:
mov [0x40100], eax
mov ebx, [0x40100]
(При этом мы используем 4 байта по адресам 0x40100, 0x40101, 0x40102, 0x40103.)
Положить в память по адресу 0x40100 4-байтное целое число 0x1543:
mov dword ptr [0x40100], 0x1543
Как правило, мы используем в качестве адресов метки:
.global main
main:
mov edi, [rip+x]
call writei32
call finish
x: .int 43
Не любой адрес в памяти доступен для чтения и тем более записи:
.global main
main:
inc [rip+x]
call finish
x: .int 43
При попытке исполнения инструкции inc
операционная система
остановит программу с сообщением «Segmentation fault»,
поскольку эти данные нельзя менять.
Секции .data и .bss
Любые байты, порождаемые ассемблером,
записываются в одну из секций исполняемого
файла. По умолчанию это секция .text
, в которой
ожидается машинный код и которая недоступна для записи.
Данные можно положить в секцию .data
:
inc [rip+x]
.data
x: .int 43
.text
call finish
Обратите внимание, что ассемблер собирает вместе содержимое каждой
из секций: в примере выше инструкция call
окажется в памяти
(и в исполняемом файле)
сразу после inc
.
Есть также секция .bss, в которую можно положить только нулевые байты:
.bss
z: .int 0
.int 0
// .int 83 // would be an error
Typical memory layout:
┌──────────────────┐ Executable file
0x00...0000│//////////////////│ ┌───────────────┐
│//////////////////│ │ headers │
├──────────────────┤ ├───────────────┤
│.text (read and │ │.text │
│ execute) │ │ │
rip─►│ │ │ │
│ │ │ │
│ │ │ │
├──────────────────┤ ├───────────────┤
│.data (read and │ │.data │
│ write) │ │ │
├──────────────────┤ └───────────────┘
│.bss (read and │
│ write) │
│ │
├──────────────────┤
│//////////////////│
│//////////////////│ /// = unmapped region
│//////////////////│ (inaccessible memory)
│//////////////////│
│//////////////////│
│//////////////////│
rsp─►│stack │
│ │
│ │
│ │
│ │
├──────────────────┤
│//////////////////│
0xff...ffff│//////////////////│
└──────────────────┘
Бывают read-only данные (например, тексты сообщений программы),
для них есть секция .rodata
. Эта секция не имеет собственной
директивы, поэтому нужно воспользоваться директивой .section
:
.section .rodata
pi_digits:
.byte 3,1,4,1,5,9,2,6
(На практике вместо .rodata
константные данные часто кладут в .text
.)
Endianness
x86 — little endian:
// 0x40100: 00 00 00 00 00 00 00 00
mov dword ptr [0x40100], 0xabcdef
// 0x40100: ef cd ab 00 00 00 00 00
Младший байт по младшему адресу в памяти.
Расширение
movzx edi, al // zero-extend, расширение нулями
movsx edi, al // sign-extend, расширение знаковым битом
cwd // convert word to double-word: sign-extend ax to dx:ax
cdq // convert double-word to quad-word: sign-extend eax to edx:eax
cqo // convert quad-word to octo-word: sign-extend rax to rdx:rax
Умножение и деление
Разные способы адресации в x86
mov eax, [rip + x] // rip-relative адресация: загрузили 4 байта по адресу x
mov eax, [rip + x + 4] // загрузили следующие 4 байта
lea rsi, [rip + x] // (положили в rsi адрес массива x)
mov eax, [rsi + 4] // косвенная адресация
mov rdi, 17
// загружаем в eax 18-й элемент массива
// (адрес которого rsi + rdi*4)
mov eax, [rsi + rdi*4]
// делаем то же самое
mov eax, [rip + x + 17*4]
.bss
x: .skip 4 * 100
.data
x5: .int x + 4*5 // в x5 лежит адрес шестого элемента массива x
Общий вид обращения к памяти:
BREG + IREG*SCALE + OFFSET
Здесь BREG — базовый регистр, IREG — индексный регистр, умножаемый на SCALE (SCALE может быть 1, 2, 4 или 8), OFFSET — непосредственно заданное смещение.
RIP-relative адресация поддерживает только OFFSET:
mov eax, [rip + 10]
mov eax, [rip + x] // неудачная нотация, но уж какая есть
Стек
Writable область памяти, которая используется как стек :-). Стек на x86 растёт вниз (от старших адресов к младшим). На верхушку стека (первый занятый байт) указывает регистр rsp.
Скопировать верхушку стека (первые 8 байт) в регистр rax
:
mov rax, [rsp]
Для того, чтобы класть данные на стек и выталкивать их оттуда, есть специальные инструкции:
push %rax // то же, что sub rsp, 8; mov [rsp], rax
pop %rax // то же, что mov rax, [rsp]; add rsp, 8
Инструкция push
одна из немногих, которые могут обратиться
к двум адресам памяти сразу:
.data
x: .quad 1900
.text
push [rip + x] // взять 8 байт из памяти по адресу x
// и положить их в стек, то есть тоже в память
Можно пользоваться памятью выше rsp
, если вы знаете,
что там что-то есть. Например, если вы положили в стек
два 64-битных числа, их можно оттуда читать и там изменять:
push 6
push 7
// теперь стек выглядит так:
// 0700 0000 0000 0000 0600 0000 0000 0000 ...
// ↑ rsp
mov rax, [rsp + 8] // загрузили в rax число 6
inc qword ptr [rsp] // теперь на верхушке стека лежит число 8
Ниже rsp
можно использовать только ближайшие 128 байт памяти (так называемая «красная зона»):
mov [rsp-8], rax // OK
mov [rsp-160], rax // не ОК, эту память могут неожиданно поменять
Инструкция push rsp
кладёт на стек
то значение rsp
, которое было до её исполнения
(не уменьшенное на 8).
Подпрограммы
Когда в ДЗ просят сдать «функцию» или «подпрограмму»:
- исполнение начинается с метки с именем подпрограммы;
- метка должна быть
.global
; - чтобы вернуть управление в проверяющую программу,
используйте инструкцию
ret
; - можно свободно менять значения регистров rax, rdi, rsi, rdx, rcx, r8–r11;
- если меняете значения других регистров, сохраняйте их в стек и потом восстанавливайте.
Например, если вас просят написать функцию foobar
,
и вам нужно менять регистры r12
и r13
:
.global foobar
foobar:
push r12
push r13
... // делаем что просят, можем портить r12 и r13
pop r13 // восстанавливаем в обратном порядке
pop r12
ret
(Подробнее в следующей лекции.)