/asm
05/09/2023 20:08
Thread №48
Posts: 8
[Open thread]
[Delete]
Изучение языка ассемблера
Хочу поделиться своими соображениями и ответами на некоторые неочевидные вопросы, которые могут возникнуть при изучении данного языка программирования, что может помочь в его понимании и сэкономить время.
Для разбора примеров буду использовать актуальную версию nasm для архитектуры x86-64 и ОС Linux.
hidden posts: 3
08/09/2023 22:48
Post №237
В продолжение темы работы с данными расскажу, как использовать массив указателей.
section .rodata
str1 db "str 1", 0xa
str2 db "str 2", 0xa
str3 db "str 3", 0xa
s_arr dq str1, str2, str3, 0 ; последний 0 - признак завершения массива
Здесь в секции .rodata размещены три символьные строки, в которых 0хa - символ завершения стоки '\n' для удобства вывода на терминал и массив указателей на строки - адреса их первого символа. Обратите внимание, что для 64-биной ОС для задания размера указателей использована псевдоинструкция dq - так как указатели имеют рамер 8 байт или 64 бита.
Как именно данные будут размещены в памяти можно увидеть из листинга (опция -l <имя_файла_листинга> nasm) данного кода:
4 00000000 73747220310A str1 db "str 1", 0xa
5 00000006 73747220320A str2 db "str 2", 0xa
6 0000000C 73747220330A str3 db "str 3", 0xa
7
8 00000012 [0000000000000000]- s_arr dq str1, str2, str3, 0
8 0000001A [0600000000000000]-
8 00000022 [0C00000000000000]-
8 0000002A 0000000000000000
Строки размещены как последовательность байт с определенных адресов. Массив указателей представляет собой последовательность адресов начала каждой из строк, кроме последнего - нулевого элемента, который является константой.
08/09/2023 23:22
Post №238
Код, который демонстрирует работу с массивом строк, определенном в предыдущем посте:
section .text
global _start
_start:
mov rdx, 6 ; длина строки
mov rdi, 1 ; запись в stdout
xor r12, r12 ; r12 = 0 первый элемент массива находится по нулевому смещению
mov r13, s_arr
print_line:
mov rsi, [s_arr + r12 * 8] ; доступ по индексу массива
test rsi, rsi ; выход в случае нулевого указателя
jz exit ; как признака конца массива
mov rax, 1 ; write
syscall
mov rsi, [r13] ; доступ по адресу элемента массива
mov rax, 1 ; write
syscall
inc r12 ; увеличить индекс на 1
add r13, 8 ; перейти к адресу следующего элемента
jmp print_line
exit:
Здесь показано два способа обращения к элементам массива: по индексу и по смещению. Для выбода на теримнал используется syscall write. Системный вызов после выполнеия возвращает в rax количество записанных символов, поэтому его приходится устанавливать в 1 заново после каждого вызова.
Самый важный момент для понимания: сам массив имеет адрес, начиная с которого в памяти расположены его элементы. Поэтому для доступа к его элементам необходимо вычислять их адреса, как смещение относительно адреса массива, используя этот адрес в качестве базы и зная размер каждого элемента - 8 байт.
21/09/2023 21:48
Post №296
Теперь, продолжая тему работы с данными, перейду к обсуждению основ использования стека. Про стек следует помнить четыре основных момента:
1. В отличии от других, зараннее определяемых секций данных, до момента выполнения программы, размер секции стека постоянно изменяется во время работы программы. Текущий размер определяет адрес, хранящийся в регистре rsp. Причем, размер стека (для x86 архитектуры) изменяется в сторону младших адресов. Данные стека занимают максимальные адреса виртуального пространства выполняемого процесса - от адреса 0x7fffffffffff и ниже.
2. Регистр rsp содержит адрес памяти откуда будут считаны данные размером (64 бит или 8 байт) при вызове команды pop.
При записи в стек командой push сначала адрес в регистре rsp уменьшается на 8, а только затем производится запись по новому адресу.
3. Изменяется размер стека кратно размеру регистра - 8 байт, то есть стек всегда выровнен относительно адресов, заканчивающихся на 0x0 и 0x8. Это всегда следует помнить при "ручном" выделении пространства в стеке, уменьшая значения регистра rsp на кратные 0x10 значения.
sub rsp, 0x40 ; зарезервировать в стеке 64 байта
21/09/2023 22:34
Post №297
Пример типичного дампа стека исполняемой программы в точке входа _start:
0x00007fffffffe420 : 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 ...............
0x00007fffffffe430 : 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 ...............
0x00007fffffffe440 : 01 00 00 00 00 00 00 00 - c3 e6 ff ff ff 7f 00 00 ...............
0x00007fffffffe450 : 00 00 00 00 00 00 00 00 - ea e6 ff ff ff 7f 00 00 ...............
0x00007fffffffe460 : fa e6 ff ff ff 7f 00 00 - 0c e7 ff ff ff 7f 00 00 ...............
0x00007fffffffe470 : 14 e7 ff ff ff 7f 00 00 - 2a e7 ff ff ff 7f 00 00 ........*......
При этом регистр указателя стека rsp содержит значение 0x00007fffffffe440. Как видно, по этому адресу записано значение 01 00 00 00 00 00 00 00, что в последовательности байт little endian, означает 0x0000000000000001 или просто 1. В данном случае это количество аргументов коммандной строки, переданных программе в момент ее запуска.
4. Так как заначение регистра rsp постоянно изменятся при использовании инструкций push и pop использовать его для досупа к данным стека неудобно. Для упрощения работы со стеком используется другой специальный регистр: rbp в котором хранится постоянное значение относительно которого можно производить адресацию. Обычно в него копируется значение rsp на момент входа в вызываемую функцию или при передачи управления коду после метки входа в программу _start. При этом, предыдущее значение rbp, если оно используется вызывающим функцию кодом, следует сохранить и не забыть восстановить при выходе из функции:
push rbp ; сохранить предыдущее знание (при необходимости)
mov rbp, rsp ; скопировать значение до использования стека
...
mov rsp, rbp ; если производилось ручное изменение rsp, то необходимо
; восстановить его начальное значение при входе
pop rbp ; восстановить предыдущее значение rbp
21/09/2023 23:20
Post №298
А теперь рассмотрим на предыдущем примере дампа, как выглядит резервирование и использование данных в стеке.
Допустим, необходимо создать две автоматические (следуя терминологии языка С) переменные: целочисленную, размером в 8 байт и строку символов из 16 байт. Для этого следует уменьшить значение rsp на 32 или 0x20 для резервирования блока памяти:
mov rbp, rsp
sub rsp, 0x20
После этого
0x00007fffffffe420 : 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 ...............
0x00007fffffffe430 : 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 ...............
0x00007fffffffe440 : 01 00 00 00 00 00 00 00 - c3 e6 ff ff ff 7f 00 00 ...............
rbp будет содержать адрес: 0xe440, а rsp 0xe420. Что означает: область в 32 байта (заполненная нулями) начиная с 0xe420 и завершая 0xe43f может быть использована программой. При этом к области возможно обратися используя отрицательное смещение относительно занчения rbp. Например:
mov qword [bp - 0x8], 1 ; записать 1 по адресу 0xe440 - 0х8 = 0xe438
mov byte [bp - 0x20], `a` ; записать символ "а" по адресу 0xe440 - 0x20 = 0xe420
Здесь смещение bp - 0x8 соответствует целочисленной переменной размером 8 байт, а 0xe420 - началу строки. При этом, что бы обратиться ко второму символу строки надо использовать дополнительное смещение от ее начала: bp - 0x20 + 1 и т.д. При этом может быть использован регистр, для обращения ко всем адресам символов строки при последовательном увеличении его значения.
Следует обратить внимание, что область памяти с нулевым смещением [bp] нельзя использовать, потому что при этом будут затерты уже существующие данные, на которые должен указывать rsp при восстановлении его предыдущего значения.
08/09/2023 23:40
Thread №51
Posts: 3
[Open thread]
[Delete]
Ассемблер - общее обсуждение
Общие обсуждения, связанные с языком ассемблера, не подпадащие под темы конкретных тредов, чтобы избежать захламления их оффтопиком.
08/09/2023 23:44
Post №239
ассемблер хорошо. особенно на МИПС архетектуре. можно писать хоумбрю для ПСП как в старые добрые времена. недавно на ПСП вышла новая прошива ARK называется и на консоль до сир пор выходят "новые" игры благодаря таким хоумбрюверам которые переводят японские игры
перенесено из треда Изучение языка ассемблера
10/09/2023 11:19
Post №241
Для меня ассемблер - это отдельный вид искусства. Давно хочется попробовать, но привязка "произведения" к архитектуре процессора отталкивает.
А если взять написание IR для LLVM, то это будет считаться "true" кодингом на ассемблере?
10/09/2023 21:53
Post №242
>>241
>Давно хочется попробовать, но привязка "произведения" к архитектуре процессора отталкивает.
Почему отталкивает? Стоит ли так опасаться "привязки", если это особенность любого низкоуровневого ЯП? Да и в общем случае, вопрос с "привязкой" (к ОС или к либам) по любому возникнет и потребует решения в том или ином виде, даже в случае использования высокоуровневого, кроссплатформенного ЯП, для проектов чуть сложнее хелловорда.