/asm

Home | Settings | Delete | Help
Язык ассемблера


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
>Давно хочется попробовать, но привязка "произведения" к архитектуре процессора отталкивает.
Почему отталкивает? Стоит ли так опасаться "привязки", если это особенность любого низкоуровневого ЯП? Да и в общем случае, вопрос с "привязкой" (к ОС или к либам) по любому возникнет и потребует решения в том или ином виде, даже в случае использования высокоуровневого, кроссплатформенного ЯП,  для проектов чуть сложнее хелловорда.