/asm/thread №48
05/09/2023 20:08
Thread №48
[Return to board]
[Delete]
Изучение языка ассемблера
Хочу поделиться своими соображениями и ответами на некоторые неочевидные вопросы, которые могут возникнуть при изучении данного языка программирования, что может помочь в его понимании и сэкономить время.
Для разбора примеров буду использовать актуальную версию nasm для архитектуры x86-64 и ОС Linux.
05/09/2023 21:07
Post №228
[Delete]
Начну со способов определения данных в тексте кода и их особенностей.
Обычно, для определения данных используются так называемые псевдоинструкции вида: db, dw, dd, dq. В случае, когда требуется резервировние неинициализированных блоков данных, используются инструкции вида: resb, resw, resd и т.п. Помимо этих двух способов для определения констант возможно использовать псевдоинструкцию equ и директиву препроцессора %define.
При этом на свойства размещаемых данных будет влияет место, где они будут размещены. Если некоторые данные размешеются в секции .data, то они будут изменяемыми - их значения, заданные во время инициализации могут быть перезаписаны другими занчениями во время выполнения программы.
section .data
var dq 10
section .text
global _start
_start:
mov rax, [var] ; можно прочитать значение области даннных
; идентифицируемой меткой var
inc rax
mov [var], rax ; можно записать новое значение в область данных var
Но, если разместить var в секции .rodata
section .rodata
var dq 10
то var станет константой и при попытке изменинить ее значение программа завершится ошибкой: segmentation fault.
05/09/2023 21:47
Post №229
[Delete]
Спешу сразу пояснить: что представляет собой идентификатор, сопоставленный с некой областью памяти. К примеру, выражение:
var dq 10
cопоставляет идентификатор var с адресом области пямяти размером в 8 байт, которая будет располагаться внутри другой области памяти, соответствующей секции статических данных, созданной во время загрузки программы на выполнение.
Надо помнить, что var - не значение перменной, как это очевидно воспринимается в ЯП высокого уровня, а адрес первого байта области памяти, используемой для размешения значения переменной. Для получения значения необходимо использовать синтаксис [var].
mov rax, var ; получаем адрес
mov rax, [var] ; получаем значение
Адреса, сопоставленные с символьными идетнтификаторами можно увидить используя утилиты предназначенные для просмотра исполнимых и объектных файлов: nm, objdump, readelf.
Например, после трансляции и линковки в полученном исполнимом файле при помощи nm можно увидеть строку:
0000000000403010 d var
Первое число будет указывать адрес по которому будут размещены данные, определяемые индентификатором var.
05/09/2023 22:30
Post №230
[Delete]
Помимо псевдоинструкций, используемых для определения данных (db, dw ...) существует инструкция equ которая отличается тем, что создет константу - имеющую значение, но не имеющую адреса.
Выражение:
a equ 10
Создает запись в служебном сегменте объектного файла .strtab, которую можно увидить при использовании nm в виде:
000000000000000a a a
Здесь первое число будет не адрес, а само занчение константы, которое напрямую будет использовано при обращенни к идентификатору.
mov rax, a
Загрузит значение 0xa (или 10) в регистр, а не адрес, которого не может существовать для такого типа данных. Это порождает путанницу, в случае непонимания различия между equ и прочими способами определения данных.
Но что бы "окончательно всех запутать" для определения значений констант возможно использовать еще один способ - комманду препроцессора %define
%define MY_CONST 10
Это значение будет подставляться во время трансляции текста программы в объектный код и попадет в объектный файл в виде подстановки - прямой замены имени на значение с ним соспоставленное. При этом оно не попадет в таблицу идентификаторов (символов) объектного и исполнимого файла. Что означет, невозможность использования таких определений вне текста программы, например во время линковки объектного файла.
08/09/2023 22:48
Post №237
[Delete]
В продолжение темы работы с данными расскажу, как использовать массив указателей.
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
[Delete]
Код, который демонстрирует работу с массивом строк, определенном в предыдущем посте:
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
[Delete]
Теперь, продолжая тему работы с данными, перейду к обсуждению основ использования стека. Про стек следует помнить четыре основных момента:
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
[Delete]
Пример типичного дампа стека исполняемой программы в точке входа _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
[Delete]
А теперь рассмотрим на предыдущем примере дампа, как выглядит резервирование и использование данных в стеке.
Допустим, необходимо создать две автоматические (следуя терминологии языка С) переменные: целочисленную, размером в 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 при восстановлении его предыдущего значения.