Skip to main content

Стек

LIFO. Управляется через регистр RSP. Когда программа начинает выполняться, ОС инициализирует регистр RSP адресом последней ячейки памяти в сегменте стека. Размер стека зависит от системы. На Linux х86-64 стек ограничен 2 мегабайтами.

Стек растет от больших адресов к меньшим. При начале стек выровнен по 16-байтовой границе.

Использование стека

push добавляет данные в стек. Возможно добавить 16- и 64-разрядный регистр, адрес в памяти 16- и 64-разрядного числа или значение 16- и 32-разрядной константы (32-битная констранта расширяется до 64 бит). 

При выполнении инструкции push от значения регистра RSP вычитается размер операнда. А по адресу, который хранится в стеке, помещается значение операнда.

pop получает из стека значение, адрес которого в регистре RSP. Можно сохранить в 16- и 64-разрядный регистр или адрес в памяти 16- и 64-разрядного числа. 

global _start
 
section .text
_start:
    mov rdx, 15
    push rdx            ; в стек помещаем содержимое регистра RDX
    pop rdi             ; значение из вершины стека помещаем в регистр RDI
    mov rax, 60
    syscall

Сохранение регистров в стек 

global _start
 
section .text
_start:
... 
    push rdi
    push rdx
...
    pop rdx
    pop rdi

Сохранение флагов состояния

Ассемблер предоставляет дополнительную пару инструкций pushfq и popfq (без аргументов) для сохранения и восстановления соответственно регистра RFLAGS (и всех флагов состояния). Например:

Восстановление стека без извлечения данных

При завершении программы следует восстановить адрес в RSP. Как выше было показано, для этого мы можем использовать инструкцию pop. Однако может сложиться ситуация, что данные не требуется извлекать из стека. Например, в зависимости от некоторых условий данные могут понадобиться, а могут не понадобиться. Если данные не нужны, извлекать каждые 8 байт отдельно с помощью инструкции pop не имеет смысла, особенно если надо извлечь много данных из стека. И в этом случае мы можем восстановить адрес в RSP, просто прибавив нужное значение - смещение относительно начального адреса. Например:

global _start
 
section .text
_start:
    mov rdi, 11
    mov rdx, 33
 
    push rdi
    push rdx
 
    add rsp, 16     ; прибавляем к адресу в RSP 16 байт 
 
    mov rax, 60
    syscall
Здесь в стек помещаем значения двух регистров - RDI и RDX, то есть адрес в RSP уменьшится на 16 байт (совокупный размер двух регистров). И чтобы быстро восстановить стек, прибавляем к адресу в RSP 16 байт:

1
add rsp, 16
Подобным образом можно вычитать из адреса в RSP определенное число, тем самым резервируя в стеке некоторое пространство:

1
2
3
4
5
6
7
8
9
10
11
global _start
 
section .text
_start:
    sub rsp, 16  ; резервируем в стеке 16 байт
     
    ; некоторая работа со стеком
 
    add rsp, 16     ; восстанавливаем значение стека
    mov rax, 60
    syscall
Поскольку вначалае вычитаем из адреса в rsp 16 байт, то после работы со стеком к адресу в rsp также прибавляется 16 байт.

Косвенная адресация в стеке
Как и в случае с любым другим регистром, в отношении регистра стека RSP можно использовать косвенную адресацию и обращаться к данным в стеке без смещения указателя RSP. Например, следующая программа на Linux помещает в стек число из регистра rdx, а затем сохраняет это число из стека в регистр rdi:

1
2
3
4
5
6
7
8
9
10
11
12
13
global _start
 
section .text
_start:
    sub rsp, 16  ; резервируем в стеке 16 байт
     
    mov rdx, 11
    mov [rsp], rdx       ; помещаем в стек значение регистра RDX
    mov rdi, [rsp]       ; в RDI помещаем значение по адресу из RSP - число 11 
 
    add rsp, 16     ; восстанавливаем значение стека
    mov rax, 60
    syscall
В данном случае в стек помещаем число из регистра RDX - число 11.

1
mov [rsp], rdx
Подобную форму размещения данных в стеке можно рассматривать как альтернативу инструкции push, если нам не надо изменять значение указателя стека RSP. То есть мы можем сохранить таким образом данные по адресу в RSP, но после этого RSP продолжает хранить тот же адрес.

Далее в регистр RDI помещаем значение, которое располагается по адресу из RSP. Фактически это тот адрес, где располагается число 11.

1
mov rdi, [rsp]
Аналогичная программа на Windows:

1
2
3
4
5
6
7
8
9
10
11
12
global _start
 
section .text
_start:
    sub rsp, 16  ; резервируем в стеке 16 байт
     
    mov rdx, 11
    mov [rsp], rdx       ; помещаем в стек значение регистра RDX
    mov rax, [rsp]       ; в RAX помещаем значение по адресу из RSP - число 11 
 
    add rsp, 16     ; восстанавливаем значение стека
    ret   
Аналогично можно применять смещения и масштабирование. Например, используем смещение в программе на Linux:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
global _start
 
section .text
_start:
    push 12
    push 13
    push 14
    push 15
 
    mov rdi, [rsp+16]      ; [rsp+16] - адрес значения 13
 
    add rsp, 32     ; восстанавливаем значение стека
 
    mov rax, 60
    syscall
Здесь в стек последовательно помещаются числа 12, 13, 14, 15. Каждое число будет занимать 8 байт. После добавления адрес в RSP будет указывать на адрес последнего добавленного числа - 15.

rsp = 0x00FF_FFD0

rsp----------------------- 0x00FF_FFD0
                |    15    | 
-------------------------- 0x00FF_FFD8
                |    14    | 
-------------------------- 0x00FF_FFE0
                |    13    | 
-------------------------- 0x00FF_FFE8
                |    12    | 
---------------------------0x00FF_FFF0
И чтобы, например, получить предыдущее число - 14, нам надо к адресу в RSP прибавить 8. А чтобы обратиться к числу 13, надо прибавить 16 байт:

1
mov rdi, [rsp+16]
Соотвественно чтобы получить из стека первое число - 12, надо к адресу в RSP прибавить 24:

1
mov rdi, [rsp+24]
Аналогичная программа на Windows:

1
2
3
4
5
6
7
8
9
10
11
12
13
global _start
 
section .text
_start:
    push 12
    push 13
    push 14
    push 15
 
    mov rax, [rsp+16]      ; [rsp+16] - адрес значения 13
 
    add rsp, 32     ; восстанавливаем значение стека
    ret   
Другой пример (на Linux):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
global _start
 
section .text
_start:
    sub rsp, 16     ; резервируем в стеке 16 байт
 
    mov rcx, 12
    mov rdx, 13
 
    mov [rsp + 8], rcx     ; [rsp + 8] = 12
    mov [rsp], rdx      ; [rsp] = 13
 
    mov rdi, [rsp]      ; rdi= 13
    add rdi, [rsp+8]     ; rdi = rdi + 12
 
    add rsp, 16     ; восстанавливаем значение стека
 
    mov rax, 60
    syscall
Здесь по адресу RSP располагается значение региста RCX, а по адресу RSP+8 - регистра RDX. В RDI извлекаем значение по адресу RSP (13), и затем складываем его со значением из RSP+8 (12). Таким образом, в RDI будет число 25.

Аналогичный пример на Windows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
global _start
 
section .text
_start:
    sub rsp, 16     ; резервируем в стеке 16 байт
 
    mov rcx, 12
    mov rdx, 13
 
    mov [rsp + 8], rcx     ; [rsp + 8] = 12
    mov [rsp], rdx      ; [rsp] = 13
 
    mov rax, [rsp]      ; rax= 13
    add rax, [rsp+8]     ; rax = rax + 12
 
    add rsp, 16     ; восстанавливаем значение стека
    ret