Push ассемблер: Дневники чайника

Дневники чайника

Дневники чайника

Стек.
Восьмой день

Стек - специально выделенная область памяти для передачи или сохранения данных.

Мы всё время наблюдаем такую форму записи (prax03.com):

00000000: B80300     mov  ax,00003
00000003: CD10       int  010
00000005: B402       mov  ah,002
00000007: 8B167501   mov  dx,[0175]
0000000B: CD10       int  010
0000000D: FEC6       inc  dh
0000000F: 80C203     add  dl,003
00000012: 89167501   mov  [0175],dx
00000016: B409       mov  ah,009
00000018: BA5001     mov  dx,00150
0000001B: CD21       int  021
0000001D: 803E760119 cmp  b,[0176],019
00000022: 75E1       jne  000000005
00000024: B410       mov  ah,010
00000026: CD16       int  016
00000028: CD20       int  020

Я имею в виду, что адреса растут вниз. 0 - выше всех (как первая строка в книге), 28 - нижняя строчка.

Так вот в этой системе отображения стек растёт вверх.

Дно стека находится по самому старшему адресу, а

вершина - по самому младшему адресу.

На вершину стека указывает регистр-указатель ESP - это его назначение (Stack Pointer - указатель стека).

Видите, prax03 в строке 12h сохраняет значение регистра DX, а в строке 07 при следующем проходе цикла обратно восстанавливает это значение. В данном примере стек использовать неудобно, но если в программе нужно много раз сохранять разные регистры, то лучше делать это через стек. Для записи в стек есть команда PUSH.

Пример:

push EDX    ; толкнуть в стек (положить в стек)
...
pop  EDX    ; извлечь из стека в EDX (вытолкнуть в EDX)

Другой пример:

push EDX    ; положить в стек
...
pop  EAX    ; извлечь из стека в EAX (в EAX будет то, что было в EDX)

Причём значений в стек можно укладывать очень много.

push EAX    ; положить в стек
push EBX    ; положить в стек
push ECX    ; положить в стек
push EDX    ; положить в стек
push 01234h ; положить в стек
. ..
pop  ESI    ; извлечь из стека (значение 01234h было сверху, значит, оно и выйдет)
pop  EDX    ; извлечь из стека в EDX
pop  ECX    ; извлечь из стека в ECX
pop  EBX    ; извлечь из стека в EBX
pop  EAX    ; извлечь из стека в EAX

Обратите внимание, что значения извлекаются в обратном порядке. Вершина выходит первой, а дно последним. На практике, слава Богу, мудрить со стеком придётся мало, но если вы не уловите принцип, то писать программы будет трудновато.

Матрос! Ну-ка иди сюда... Что тут у тебя в корзинке? О, завтрак! Так, сверху яйца, подержи, бутерброд, бутылка... подушка? А зачем тебе подушка? Ты что, на вахте спать собрался! А ну, отдай сюда корзину. Яйца-то сырые, чёрт тебя дери! И бутерброды измазал, и подушку испортил. Какого шлейфа ты их не сварил? Иди отсюда, сам знаю, что яйца сверху кладут.

Память под стек выделяет Windows.

Теперь осмысленно вернёмся к нашей первой программе под форточки:


Адреса   байты         имена операнды                          комментарии
00401000 6A 00         push  0                                ; /Style = MB_OK|MB_APPLMODAL
00401002 68 00304000   push  00403000                         ; |Title = "It's the first your program for Win32"
00401007 68 21304000   push  00403021                         ; |Text = "Assembler language for Windows is a fable!"
0040100C 6A 00         push  0                                ; |hOwner = NULL
0040100E E8 07000000   call &lt jmp.
&user32.MessageBoxA&gt ; \MessageBoxA 00401013 6A 00 push 0 ; /ExitCode = 0 00401015 E8 06000000 call &lt jmp.&kernel32.ExitProcess&gt ; \ExitProcess 0040101A FF25 08204000 jmp dword ptr ds:[&lt user32.MessageBo&gt ; user32.MessageBoxA 00401020 FF25 00204000 jmp dword ptr ds:[&lt kernel32.ExitPro&gt ; kernel32.ExitProcess

Команда Push кладёт в стек указанное в ней значение. Оно может быть два байта или четыре. В 32-битных программах это будет 32-битное значение, то есть dword (4 байта). Так что для нас теперь одно значение в стеке 4 байта.

Push 0        ; отправляет в стек значение 00000000
Push 00403000 ; отправляет в стек значение 00403000
Push 00403021 ; отправляет в стек значение 00403021
Push 0        ; отправляет в стек значение 00000000

Это, естественно, целое и для "удобства" чтения, младший байт уже справа, старший слева.

Матрос, ты запомнил, что на наших чертежах в целой боевой единице Бинарников байты строятся по старшинству слева направо? Каждый младший - самый левый, каждый старший - самый правый (на фиг такие чертёжи :).
У них там в главном штабе есть ещё запутка - стек. Он в чертежах идёт от дна наверх. Целые (word или dword) в нём укладываются именно так: от дна наверх.

Действительно, стек проще представлять в высоту, чем в одну строку. Вот что будет содержаться в стеке перед выполнением строки 40100Eh в нашей программе.

00000000
00403021
00403000
00000000
........

Каждая строка здесь - одно значение в стеке. Понятие дно очень наглядно объясняет, как устроен стек.

Давайте удалим из исходника эту строку:

	invoke MessageBox, NULL, addr MsgBoxText, addr MsgBoxCaption, MB_OK

И впишем вместо неё вот такие команды:

  push 0AAAAAAAAh
  push 0BBBBBBBBh
  push 0CCCCCCCCh
  push 0DDDDDDDDh

  pop  EAX
  pop  ECX
  pop  EDX
  pop  EBX

Соберите программу ещё раз. Откройте в Olly.

То, что вы делали в отладчике CodeView клавишей F10, называется по-умному:
пошаговая трассировка исполняемого кода без захода в процедуры. В Olly такой "шаг" выполняет клавиша F8.


Адреса   Байты        Имена Операнды
00401000 68 AAAAAAAA  push AAAAAAAA
00401005 68 BBBBBBBB  push BBBBBBBB
0040100A 68 CCCCCCCC  push CCCCCCCC
0040100F 68 DDDDDDDD  push DDDDDDDD
00401014 58           pop  eax
00401015 59           pop  ecx
00401016 5A           pop  edx
00401017 5B           pop  ebx

Когда вы дойдёте до строки 401014h (F8 четыре раза), стек будет выглядеть вот так:

0012FFB4  DDDDDDDD
0012FFB8  CCCCCCCC
0012FFBC  BBBBBBBB
0012FFC0  AAAAAAAA
...        ...

Посмотрите в отладчике, обязательно! Нижняя правая часть.

Вы должны были понять, что каждое новое значение укладывается сверху. Получается, стек растёт вверх. Получается, регистр ESP с каждым новым значением стека уменьшается. И каждый раз, когда из стека извлекают значение, ESP увеличивается на 4 (или на 2 в 16-битных программах).

А ещё стек нужно выравнивать, иначе программа будет работать неправильно. После того, как ваши данные в стеке отработали или просто больше не нужны, возвращайте вершину в положение, которое она занимала раньше - это и есть выравнивание. Если вы так не сделаете, программа вызовет ошибку.

Стек, пожалуй, самое сложное понятие, которое нужно усвоить для написания программ на Ассемблере. Честно-честно, если вы поймёте это, значит, вы точно сможете освоить всё остальное. Конечно, нужна практика, помню, я три дня потратил на понимание стека. Калашников обманул, сказал, что это просто. Так и было, когда я его примеры использовал, а вот когда свои программки стал писать, стек вызвал наибольшие затруднения.

Три дня - думаю, это немного для самого сложного фокуса.

Чтоб положить что-то в стек, пишите:

push что-то
Команда PUSH
ПроисхождениеОт англ. слова push - толкать
Форматpush операнд
ДействиеТолкает значение в стек
ПримечаниеЕсли нужно толкнуть в стек все 8 регистров общего назначения (EAX, EBX, ECX, EDX, EBP, ESP, ESI, EDI), то лучше использовать одну команду PUSHA/PUSHAD

Чтоб достать число с "верхушки стека" куда-то, пишите:

pop куда-то
Команда POP
ПроисхождениеОт англ. слова pop - извлекать, раскошеливаться
ФорматPOP операнд
ДействиеИзвлекает значение из стека
ПримечаниеЕсли нужно достать из стека все РОН, уложенные командой PUSHA, используйте POPA/POPAD

Следующий пример (prax06.com).

Наберите в Hiew'e эти байты:

B8 CD 20 50-EB FD

Теперь посмотрите на асмовый вид:


Адреса    Байты   Имена   Операнды
00000000: B8CD20  mov     ax,020CD 
00000003: 50      push    ax
00000004: EBFD    jmps    000000003

Давайте проанализируем этот пример.


00000000: B8CD20  mov     ax,020CD 

Эта строка помещает в регистр AX значение 20CD.


00000003: 50      push    ax

Эта строка кладёт в стек word (слово) 20CDh из регистра EAX. Причём в памяти слово вы увидите как положено - CD,20h.


00000004: EBFD    jmps    000000003

А вот эта строка зацикливает выполнение строки с адресом 03. То есть после выполнения строки 03 в строке 04 будет совершаться безусловный переход к строке 03.


Так почему же этот пример не вешает комп в ДОС и не виснет сам в WinXP? Запустите и вы увидите, что прога выполнится за доли секунды.

Можете попробовать разобрать хитрость в CV сами, но это займёт некоторое время :).

Честно скажу, что отладчик типа CodeView не очень подходит для разбора этого пустячка.

Дело в том, что простые DOS-отладчики используют стек программы в своих целях, что сбивает значение регистра ESP между шагами. Здесь бы лучше всего запустить пример под SoftIce'ом. И тогда вы могли бы "ровно" увидеть, как программа 32637 раз запишет в стек два байта CD20h. Последний раз байты будут вписаны вместо команды "jmp 03".

CD20h - это машинный код команды int 20h (вызов прерывания завершения программ, как вы уже знаете).

Стек в com-программе начинается на дне её сегмента (FFFE - последний адрес, кратный двум).

Получается, что стек всё растёт и растёт вверх и ничего его не остановит, кроме завершающего или ошибочного кода.

И вот он заполняет всё свободное место в сегменте, а затем начинает затирать данные (в этом примере их нет). Далее затирается код программы.

Такая ситуация называется ошибкой переполнения стека. Для DOS-программ данная ошибка - одна из самых распространённых.

В Win32-программах механизм выделения стека более развит (ОСь сама занимается этим вопросом, используя средства защищённого режима). Однако ошибки переполнения стеков - всё ещё настоящий вызов для программистов, особенно в сфере безопасности. Вы только что могли наблюдать, как из-за такой ошибки стек начинает выполняться.

Ведь изначально программа просто отправляла данные CD20h в стек. Но затем стек вырос (адрес в ESP уменьшился!) до значения, равного адресу команды JMP 0103 (в CodeView этого не видно!). И вот, после следующего выполнения команды push AX, происходит чудесное превращение данных в исполняемый код.

В CodeView тоже можно проследить самый интересный момент этого примера. Измените регистр указателя стека (ESP). Например, установите в нём значение 200h (чтоб долго не трейсить). После этого - F10 много раз, и прога завершится. Посмотрите ещё и ещё раз. Может быть, вы придумаете, как такой фокус можно использовать в своих целях ;).

Предлагаю вам ещё один пример Win32.

Учтите, что русские буквы должны быть в кодировке ANSI (стандарт для форточек).

prax07.asm:

      .386
      .model flat, stdcall
      option casemap :none   ; case sensitive
;#########################################################################
      include \masm32\include\windows. inc
      include \masm32\include\user32.inc
      include \masm32\include\kernel32.inc
      includelib \masm32\lib\user32.lib
      includelib \masm32\lib\kernel32.lib
;#########################################################################
    .data
	MsgBoxCaption  db "It's my first command line for Win32",0
	MsgBoxText     db "Аргументы командной строки отсутствуют",0
;#########################################################################
    .code

start:
	call   GetCommandLine ; API. Возвращает в EAX адрес командной строки
	mov    ECX,512d       
	add    ECX,EAX        

   unquote:	              ; Метка цикла нахождения закрывающей кавычки
	inc    EAX
	cmp    EAX,ECX
	jz     NO
	cmp    byte ptr[EAX],22h
   jnz unquote

   Arg_search:               ; Метка цикла нахождения аргумента командной строки
	inc    EAX              
	cmp    byte ptr[EAX],0  
	jz     NO               
	cmp    byte ptr[EAX],20h
   jz  Arg_search               



  push   0
  push   offset MsgBoxCaption
  push   EAX
  push   0
  call   MessageBox           ; Вызов API-функции вывода сообщения на экран

  push   0                    ; Пустой параметр для функции выхода
  call   ExitProcess          ; Вызов API-функции выхода

NO:
  push   0                    ; Параметр "MB_OK"
  push   offset MsgBoxCaption ; Параметр "адрес заголовка"
  push   offset MsgBoxText    ; Параметр "адрес текста сообщения"
  push   0                    ; Параметр "родительское окно"
  call   MessageBox           ; Вызов API-функции вывода сообщения на экран

  push   0                    ; Пустой параметр для функции выхода
  call   ExitProcess          ; Вызов API-функции выхода

end start

Наберите, пожалуйста, сами секцию кода. Я уверен, что у вас будут опечатки, и это очень ускорит процесс обучения.

Для того, чтоб пример сделал то, что ему положено, запустите его с каким-нибудь ключом типа:
prax07.exe qwerty

Ну что ж, отладчик Olly - ваш лучший друг. При открытии впишите ключ в поле "Arguments". Не торопитесь, подумайте что к чему.

Здесь используются 3 API-функции:

  • GetCommandLine - возвращает в регистр EAX адрес командной строки программы вместе с путём в кавычках. Например:
    "D:\tut\prax07.exe " qwerty
  • MessageBox - вызывает функцию вывода сообщений на экран.
  • ExitProcess - функция завершения.

Осталась только одна команда, которую вы пока не знаете в этой программе. Команда CALL, но это целая тема. И, между прочим, последняя теоретическая тема в наших уроках.

Bitfry


Написание и отладка кода на ассемблере x86/x64 в Linux

Сегодня мы поговорим о программировании на ассемблере. Вопрос «зачем кому-то в третьем тысячелетии может прийти в голову писать что-то на ассемблере» раскрыт в заметке Зачем нужно знать всякие низкоуровневые вещи, поэтому здесь мы к нему возвращаться не будем. Отмечу, что в рамках поста мы сосредоточимся на вопросе компиляции и отладки программ на ассемблере. Сам же язык ассемблера заслуживает отдельного большого поста, а то и серии постов.

Если вы знаете ассемблер, то любая программа для вас — open source.

Народная мудрость.

Введение

Существует два широко используемых ассемблерных синтаксиса — так называемые AT&T-синтаксис и Intel-синтаксис. Они не сильно друг от друга отличаются и легко переводятся один в другой. В мире Windows принято использовать синтаксис Intel. В мире *nix систем, наоборот, практически всегда используется синтаксис AT&T, а синтаксис Intel встречается крайне редко (например, он используется в утилите perf). Поскольку Windows, как известно, не существует, далее мы сосредоточимся на правильном AT&T-синтаксисе 🙂

Компиляторов ассемблера существует много. Мы будем использовать GNU Assembler (он же GAS, он же /usr/bin/as). Скорее всего, он уже есть вашей системе. К тому же, если вы пользуетесь GCC и собираетесь писать ассемблерные вставки в коде на C, то именно с этим ассемблером вам предстоит работать. Из достойных альтернатив GAS можно отметить NASM и FASM.

Наконец, язык ассемблера отличается в зависимости от архитектуры процессора. Пока что мы сосредоточимся на ассемблере для x86 (он же i386) и x64 (он же amd64), так как именно с этими архитектурами приходится чаще всего иметь дело. Впрочем, ARM тоже весьма распространен, главным образом на телефонах и планшетах. Еще из сравнительно популярного есть SPARC и PowerPC, но шансы столкнуться с ними весьма малы. Отмечу, что x86 и x64 можно было бы рассматривать отдельно, но эти архитектуры во многом похожи, поэтому я не вижу в этом большого смысла.

«Hello, world» на int 0x80

Рассмотрим типичный «Hello, world» для архитектуры x86 и Linux:

.data
msg:
  . ascii "Hello, world!\n"
  .set len, . - msg

.text

.globl _start
_start:
  # write
  mov $4,   %eax
  mov $1,   %ebx
  mov $msg, %ecx
  mov $len, %edx
  int $0x80

  # exit
  mov $1,   %eax
  xor %ebx, %ebx
  int $0x80

Компиляция:

# Или: gcc -m32 -c hello-int80.s
as --32 hello-int80.s -o hello-int80.o
ld -melf_i386 -s hello-int80.o -o hello-int80

Коротко рассмотрим первые несколько действий, выполняемых программой: (1) программа начинает выполнение с метки _start, (2) в регистр eax кладется значение 4, (3) в регистр ebx помещается значение 1, (4) в регистр ecx кладется адрес строки, (5) в регистр edx кладется ее длина, (6) происходит прерывание 0x80. Так в мире Linux традиционно происходит выполнение системных вызовов. Конкретно int 0x80 считается устаревшим и медленным, но из соображений обратной совместимости он все еще работает. Далее мы рассмотрим и более новые механизмы.

Нетрудно догадаться, что eax — это номер системного вызова, а ebx, ecx и edx — его аргументы. Какой системный вызов имеет какой номер можно подсмотреть в файлах:

# для x86
/usr/include/x86_64-linux-gnu/asm/unistd_32.h
# для x64
/usr/include/x86_64-linux-gnu/asm/unistd_64.h

Следующая строчка из файла unistd_32.h:

… как бы намекает нам, что производится вызов write. В свою очередь, из man 2 write мы можем узнать, какие аргументы этот системный вызов принимает:

#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);

То есть, рассмотренный код эквивалентен:

// напомню, что stdout == 1
write(stdout, "Hello, world!\n", 14)

Затем аналогичным образом производится вызов:

// команда `xor %ebx, %ebx` обнуляет регистр %ebx
exit(0)

Совсем не сложно!

В общем случае системный вызов через 0x80 производится по следующим правилам. Регистру eax присваивается номер системного вызова из unistd_32. h. До шести аргументов помещаются в регистры ebx, ecx, edx, esi, edi и ebp. Возвращаемое значение помещается в регистр eax. Значения остальных регистров при возвращении из системного вызова остаются прежними.

Выполнение системного вызова через sysenter

Начиная с i586 появилась инструкция sysenter, специально предназначенная (чего нельзя сказать об инструкции int) для выполнения системных вызовов.

Рассмотрим пример использования ее на Linux:

.data
msg:
  .ascii "Hello, world!\n"
  len = . - msg

.text
.globl _start

_start:
  # write
  mov   $4,   %eax
  mov   $1,   %ebx
  mov   $msg, %ecx
  mov   $len, %edx
  push  $write_ret
  push  %ecx
  push  %edx
  push  %ebp
  mov   %esp, %ebp
  sysenter

write_ret:
  # exit
  mov   $1,   %eax
  xor   %ebx, %ebx
  push  $exit_ret
  push  %ecx
  push  %edx
  push  %ebp
  mov   %esp, %ebp
  sysenter

exit_ret:

Сборка осуществляется аналогично сборке предыдущего примера.

Как видите, принцип тот же, что при использовании int 0x80, только перед выполнением sysenter требуются поместить в стек адрес, по которому следует вернуть управление, а также совершить кое-какие дополнительные манипуляции с регистрами. Причины этого более подробно объясняются здесь.

Инструкция sysenter работает быстрее int 0x80 и является предпочтительным способом совершения системных вызовов на x86.

Выполнение системного вызова через syscall

До сих пор речь шла о 32-х битных программах. На x64 выполнение системных вызовов осуществляется так:

.data
msg:
  .ascii "Hello, world!\n"
  .set len, . - msg

.text

.globl _start
_start:
  # write
  mov  $1,   %rax
  mov  $1,   %rdi
  mov  $msg, %rsi
  mov  $len, %rdx
  syscall

  # exit
  mov  $60, %rax
  xor  %rdi, %rdi
  syscall

Собирается программа таким образом:

as --64 hello-syscall.s -o hello-syscall. o
ld -melf_x86_64 -s hello-syscall.o -o hello-syscall

Принцип все тот же, но есть важные отличия. Номера системных вызовов нужно брать из unistd_64.h, а не из unistd_32.h. Как видите, они совершенно другие. Так как это 64-х битный код, то и регистры мы используем 64-х битные. Номер системного вызова помещается в rax. До шести аргументов передается через регистры rdi, rsi, rdx, r10, r8 и r9. Возвращаемое значение помещается в регистр rax. Значения, сохраненные в остальных регистрах, при возвращении из системного вызова остаются прежними, за исключением регистров rcx и r11.

Интересно, что в программе под x64 можно одновременно использовать системные вызовы как через syscall, так и через int 0x80.

Отладка ассемблерного кода в GDB

Статья была бы не полной, если бы мы не затронули вопрос отладки всего этого хозяйства. Так как мы все равно очень плотно сидим на GNU-стэке, в качестве отладчика воспользуемся GDB. По большому счету, отладка не сильно отличается от отладки обычного кода на C, но есть нюансы.

Например, вы не можете так просто взять и поставить брейкпоинт на процедуру main. Как минимум, у вас попросту нет отладочных символов с информацией о том, где эту main искать. Решение заключается в том, чтобы самостоятельно определить адрес точки входа в программу и поставить брейкпоинт на этот адрес:

Увидим что-то вроде:

[...]
    Entry point: 0x4000b0
[...]

Далее говорим:

Какого-либо исходного кода у нас тоже нет, поэтому команда l работать не будет. Сами ассемблерные инструкции и есть исходный код! Так, например, можно посмотреть следующие 5 ассемблерных инструкций:

По понятным причинам, переход к очередной строчке кода при помощи команд n или s работать не будет. Вместо этих команд следует использовать команды перехода к следующей инструкции — ni, si, и так далее.

Смотреть и изменять значения переменных мы тоже не можем. Однако ничто не мешает смотреть и изменять значения регистров:

info registers
p/x $rcx
p $xmm1
set $r15 = 0x123

Наконец, стектрейсы нам тоже недоступны. Но ничто не мешает, например, посмотреть 8 ближайших значений на стеке:

По большому счету, это все отличие от отладки программы на C при наличии исходников. Кстати, вы можете легко посмотреть, в какой ассемблерных код транслируется ваш код на C, одним из следующих способов:

gcc -S test.c -o -
objdump -d ./myprog

Как альтернативный вариант, можно воспользоваться Hopper или подобным интерактивным дизассемблером.

Внезапно отладка программы, собранной без -g и/или с -O2, перестала казаться таким уж страшным делом, не так ли?

Заключение

В качестве домашнего задания можете попытаться написать программу на ассемблере, выводящую переменные окружения, а также переданные ей аргументы командной строки.

Примите во внимание, что в Linux есть еще как минимум два способа сделать системный вызов — через так называемые vsyscall (считается устаревшим, но поддерживается для обратной совместимости) и VDSO (пришедший ему на замену). Эти способы основаны на отображении страницы ядра в адресное пространство процесса и призваны ускорить выполнение системных вызовов, не требующих проверки привилегий и других тяжелых действий со стороны ядра системы. В качестве примера вызова, который может быть ускорен таким образом, можно привести gettimeofday. К сожалению, рассмотрение vsyscall и VDSO выходит за рамки данного поста. Больше информации о них вы найдете по приведенным ниже ссылкам.

Ссылки по теме:

Кроме того, вас могут заинтересовать статьи, посвященные ассемблеру, в замечательных блогах alexanius-blog.blogspot.ru и 0xax.blogspot.ru.

А какие инструменты (компилятор, отладчик, …) вы предпочитаете использовать для программирования на ассемблере?

Дополнение: Шпаргалка по основным инструкциям ассемблера x86/x64

Метки: Linux, Ассемблер.

ASSEMBLER&WIN32. КУРС МОЛОДОГО БОЙЦА. УРОК 3.mov — команда ассемблера

Когда вы пишете программу на ассемблере, вы просто пишете команды процессору. Команды процессору — это просто коды или коды операций или опкоды. Опкоды — фактически «читаемый текст»- версии шестнадцатеричных кодов. Из-за этого, ассемблер считается самым низкоуровневым языком программирования, все в ассемблере непосредственно преобразовывается в шестнадцатеричные коды. Другими словами, у вас нет компилятора, который преобразовывает язык высокого уровня в язык низкого уровня, ассемблер только преобразовывает коды ассемблера в данные.

В этом уроке мы обсудим несколько опкодов, которые имеют отношение к вычислению, поразрядным операциям, и т.д. Другие опкоды: команды перехода, сравнения и т.д, будут обсуждены позже.

Комментарии в ваших программах оставляются после точки с запятой. Точно также как в дельфи или си через //.
Числа в ассемблере могут представляться в двоичной, десятеричной или шестнадцатеричной системе. Для того, чтобы показать в какой системе использовано число надо поставить после числа букву. Для бинарной системы пишется буква b (пример: 0000010b, 001011010b), для десятеричной системы можно ничего не указывать после числа или указать букву d (примеры: 4589, 2356d), для шестнадцатеричной системы надо указывать букву h, шестнадцатеричное число надо обязательно писать с нулём в начале (примеры: 00889h, 0AC45h, 056Fh, неправильно F145Ch, С123h).

Самая первая команда будет хорошо всем известная MOV. Эта команда используется для копирования (не обращайте внимания на имя команды) значения из одного места в другое. Это ‘место’ может быть регистр, ячейка памяти или непосредственное значение (только как исходное значение). Синтаксис команды:

mov приемник, источник

Вы можете копировать значение из одного регистра в другой.

mov edx, ecx

Вышеприведенная команда копирует содержание ecx в edx. Размер источника и приемника должны быть одинаковыми, например: эта команда — НЕ допустима:

mov al, ecx ; не правильно

Этот опкод пытается поместить DWORD (32-битное) значение в байт (8 битов). Это не может быть сделано mov командой (для этого есть другие команды).

А эти команды правильные, потому что у них источник и приемник не отличаются по размеру:

mov al, bl
mov cl, dl
mov cx, dx
mov ecx, ebx

Вы также можете получить значение из памяти и поместить эго в регистр. Для примера возьмем следующую схему памяти:

 

смещение 34 35 36 37 38 39 3A 3B 3C 3D 3E 3F 40 41 42
данные 0D 0A 50 32 44 57 25 7A 5E 72 EF 7D FF AD C7

(Каждый блок представляет байт)

Значение смещения обозначено здесь как байт, но на самом деле это это — 32-разрядное значение. Возьмем для примера 3A, это также — 32-разрядное значение: 0000003Ah. Только, чтобы с экономить пространство, некоторые используют маленькие смещения.

Посмотрите на смещение 3A в таблице выше. Данные на этом смещении — 25, 7A, 5E, 72, EF, и т.д. Чтобы поместить значение со смещения 3A, например, в регистр, вы также используете команду mov:

mov eax, dword ptr [0000003Ah]

Означает: поместить значение с размером DWORD (32-бита) из памяти со смещением 3Ah в регистр eax. После выполнения этой команды, eax будет содержать значение 725E7A25h. Возможно вы заметили, что это — инверсия того что находится в памяти: 25 7A 5E 72. Это потому, что значения сохраняются в памяти, используя формат little endian . Это означает, что самый младший байт сохраняется в наиболее значимом байте: порядок байтов задом на перед. Я думаю, что эти примеры покажут это:

dword (32-бит) значение 10203040 шестнадцатиричное сохраняется в памяти как: 40, 30, 20, 10
word (16-бит) значение 4050 шестнадцатиричное сохраняется в памяти как: 50, 40

Вернемся к примеру выше. Вы также можете это делать и с другими размерами:

mov cl, byte ptr [34h] ; cl получит значение 0Dh
mov dx, word ptr [3Eh] ; dx получит значение 7DEFh

Вы, наверное, уже поняли, что префикс ptr обозначает, что надо брать из памяти некоторый размер. А префикс перед ptr обозначает размер данных:
Byte — 1 байт
Word — 2 байта
Dword — 4 байта

Иногда размер можно не указывать:

mov eax, [00403045h]

Так как eax — 32-разрядный регистр, ассемблер понимает, что ему также требуется 32-разрядное значение, в данном случае из памяти со смещением 403045h.

Можно также непосредственные значения:

mov edx, 5006h

Эта команда просто запишет в регистр edx, значение 5006. Скобки, [ и ], используются, для получения значения из памяти (в скобках находится смещение), без скобок, это просто непосредственное значение.

Можно также использовать регистр как ячейку памяти (он должен быть 32-разрядным в 32-разрядных программах):

mov eax, 403045h ; пишет в eax значение 403045
mov cx, [eax] ; помещает в регистр CX значение (размера word) из памяти
; указанной в EAX (403045)

В mov cx, [eax], процессор сначала смотрит, какое значение (= ячейке памяти) содержит eax, затем какое значение находится в той ячейке памяти, и помещает это значение (word, 16 бит, потому что приемник, cx, является 16-разрядным регистром) в CX.

Стековые операции — PUSH, POP . Перед тем, как рассказать вам о стековых операциях, я уже объяснял вам, что такое стек. Стек это область в памяти, на которую указывает регистр стека ESP. Стек это место для хранения адресов возврата и временных значений. Есть две команды, для размещения значения в стеке и извлечения его из стека: PUSH и POP. Команда PUSH размещает значение в стеке, т.е. помещает значение в ячейку памяти, на которую указывает регистр ESP, после этого значение регистра ESP увеличивается на 4. Команда Pop извлекает значение из стека, т.е. извлекает значение из ячейки памяти, на которую указывает регистр ESP, после этого уменьшает значение регистра ESP на 4. Значение, помещенное в стек последним, извлекается первым. При помещении значения в стек, указатель стека уменьшается, а при извлечении — увеличивается. Рассмотрим пример:

(1) mov ecx, 100
(2) mov eax, 200
(3) push ecx ; сохранение ecx
(4) push eax
(5) xor ecx, eax
(6) add ecx, 400
(7) mov edx, ecx
(8) pop ebx
(9) pop ecx

Анализ:
1: поместить 100 в ecx
2: поместить 200 в eax
3: разместить значение из ecx (=100) в стеке (размещается первым)
4: разместить значение из eax (=200) в стеке (размещается последним)
5/6/7: выполнение операций над ecx, значение в ecx изменяется
8: извлечение значения из стека в ebx: ebx станет 200 (последнее размещение, первое извлечение)
9: извлечение значения из стека в ecx: ecx снова станет 100 (первое размещение, последнее извлечение)

Чтобы узнать, что происходит в памяти, при размещении и извлечении значений в стеке, см. на рисунок ниже:

Смещение 1203 1204 1205 1206 1207 1208 1209 120A 120B
Значение 00 00 00 00 00 00 00 00 00
ESP

(стек здесь заполнен нулями, но в действительности это не так, как здесь). ESP стоит в том месте, на которое он указывает)

mov ax, 4560h
push ax

Смещение 1203 1204 1205 1206 1207 1208 1209 120A 120B
Значение 00 00 60 45 00 00 00 00 00
ESP

mov cx, FFFFh
push cx

Смещение 1203 1204 1205 1206 1207 1208 1209 120A 120B
Значение FF FF 60 45 00 00 00 00 00
ESP

pop edx

Смещение 1203 1204 1205 1206 1207 1208 1209 120A 120B
Значение FF FF 60 45 00 00 00 00 00
ESP

edx теперь 4560FFFFh.

Вызов подпрограмм возврат из них — CALL, RET. Команда call передает управление ближней или дальней процедуре с запоминанием в стеке адреса точки возврата. Команда ret возвращает управление из процедуры вызывающей программе, адрес возврата получает из стека. Пример:

..code..
call 0455659
..more code..
Код с адреса 455659:
add eax, 500
mul eax, edx
ret

Когда выполняется команда call, процессор передает управление на код с адреса 455659, и выполняет его до команды ret, а затем возвращает управление команде следующей за call. Код который вызывается командой call называется процедурой. Вы можете поместить код, который вы часто используете в процедуру и каждый раз когда он вам нужен вызывать его командой call.

Подробнее: команда call помещает регистр EIP (указатель на следующюю команду, которая должна быть выполнена) в стек, а команда ret извлекает его и передаёт управление этому адресу. Вы также можете определить аргументы, для вызываемой программы (процедуры). Это можно сделать через стек:

push значение_1
push значение_2
call procedure

Внутри процедуры, аргументы могут быть прочитаны из стека и использованы. Локальные переменные, т.е. данные, которые необходимы только внутри процедуры, также могут быть сохранены в стеке. Я не буду подробно рассказывать об этом, потому, что это может быть легко сделано в ассемблерах MASM и TASM. Просто запомните, что вы можете делать процедуры и что они могут использовать параметры.

Одно важное замечание:
регистр eax почти всегда используется для хранения результата процедуры.
Это также применимо к функциям windows. Конечно, вы можете использовать любой другой регистр в ваших собственных процедурах, но это стандарт.

Первая команда в ассемблере MOV

Эта команда используется для копирования значения из приёмника в источник. Синтаксис команды:

Mov приемник, источник

 

Пример:

Mov edx, ecx ; правильно

 

Размер источника и приемника должны быть одинаковыми.

Mov al, ecx; не правильно

 

Этот код пытается поместить DWORD (32-битное) значение в байт (8 битов).

 

Правильные команды:

Mov al, bl

Mov cl, dl

Mov cx, dx

Mov ecx, ebx

 

Можно получить значение из памяти и поместить его в регистр. Например, имеем следующую схему памяти:

смещение 3A 3B 3C 3D 3E 3F
данные 0D 0A 7A 5E EF 7D FF AD C7

 

Данные, которые имеют смещение 3A: 25, 7A, 5E, 72, EF, и т. д. Чтобы поместить значение со смещения 3A, например, в регистр, можно воспользоваться командой mov:

mov eax, dword ptr [0000003Ah] ; eax=725E7A25h

 

При работе с памятью самый младший байт сохраняется в наиболее значимом байте: порядок байтов задом на перед.

 

dword (32-бит) значение 10203040 шестнадцатиричное сохраняется в памяти как: 40, 30, 20, 10

word (16-бит) значение 4050 шестнадцатиричное сохраняется в памяти как: 50, 40

 

Пример2:

mov cl, byte ptr [34h] ; cl получит значение 0Dh

mov dx, word ptr [3Eh] ; dx получит значение 7DEFh

 

Размеры данных для префикса ptr:

Byte - 1 байт

Word - 2 байта

Dword - 4 байта

 

Размер можно не указывать:

mov eax, [00403045h]

 

 

Пример3:

mov eax, 403045h ; eax= 00403045h

mov cx, [eax] ; CX=значение (размера word) из памяти указанной в EAX (403045)

 


Стековые операции - PUSH, POP.

Стек это область в памяти, на которую указывает регистр стека ESP(SP). Есть две команды, для размещения значения в стеке и извлечения его из стека: PUSH и POP. Команда PUSH размещает значение в стеке, т.е. помещает значение в ячейку памяти, на которую указывает регистр ESP, после этого значение регистра ESP уменьшается на 4. Команда POP извлекает значение из стека, т.е. извлекает значение из ячейки памяти, на которую указывает регистр ESP, после этого увеличивается значение регистра ESP на 4. Значение, помещенное в стек последним, извлекается первым.

Пример:

(1) mov ecx, 100

(2) mov eax, 200

(3) push ecx

(4) push eax

(5) xor ecx, eax

(6) add ecx, 400

(7) mov edx, ecx

(8) pop ebx

(9) pop ecx

 

Анализ:

1: поместить 100 в ecx

2: поместить 200 в eax

3: разместить значение из ecx (=100) в стеке (размещается первым)

4: разместить значение из eax (=200) в стеке (размещается последним)

5/6/7: выполнение операций над ecx, значение в ecx изменяется

8: извлечение значения из стека в ebx: ebx=200.

9: извлечение значения из стека в ecx: ecx=100.

 

Пример: Работа со стеком.

Смещение 120A 120B
Значение
          ESP        

ESP стоит в том месте, на которое он указывает)

Mov ax, 4560h

Push ax

Смещение 120A 120B
Значение
      ESP            

Mov cx, FFFFh

Push cx

Смещение 120A 120B
Значение FF FF
  ESP                

Pop edx

Смещение 120A 120B
Значение FF FF
          ESP        

edx = 4560FFFFh.

Пересылка данных

Поддерживаются начиная с процессора 8086.
mov <операнд_назначения>, <операнд_источник>

xchg <операнд1>, <операнд2>

cmovcc <приёмник><источник>

bswap <регистр 32>

 

Особенности команды mov:

1) нельзя осуществлять пересылку из одной области памяти в другую. При такой необходимости нужно использовать в качестве промежуточного буфера любой доступный регистр общего назначения. Пример: переслать байты из ячейки памяти fls в ячейку fld:

Data

Fls dd 947503b3h

Fld dd ?

Code

Start

----

Mov eax, fls

Mov fld,eax

----

End start

 

2) нельзя загрузить в сегментный регистр значение непосредственно из памяти. Для выполнения такой загрузки нужно использовать промежуточный объект (регистр общего назначения или стек).

 

3) нельзя пересылать содержимое одного сегментного регистра в другой сегментный регистр. Выполнить такую пересылку можно, используя в качестве промежуточных регистры общего назначения.

Пример: инициализировать регистр es значением регистра ds:

Mov ax,ds

Move es,ax

Можно также использовать стек и команды push и pop:

Push ds ; поместить значение регистра ds в стек

Pop es ; записать в es число из стека

 

Нельзя использовать сегментный регистр cs в качестве операнда назначения.

 

5) оператор ptr можно применять и когда требуется принудительно поменять размерность операндов. К примеру, требуется переслать значение 0ffh во второй байт поля flp:

Data

Flp dw ?

Code

start:

----

mov byte ptr (flp+1),0ffh

----

End start

 

Для двунаправленной пересылки данных применяют команду xchg. Эту же операцию можно выполнить применив последовательность из нескольких команд mov. Общий вид записи:

 

XCHG <операнд1>, <операнд2>

 

Содержимое операнда2 копируется в операнд1, а старое содержимое операнда1 — в операнд2. XCHG можно выполнять над двумя регистрами или над регистром и переменной.

Например:

Xchg eax,ebx ; обменять содержимое регистров eax и ebx.

То же, что три команды на языке С:

temp = eax;

eax = ebx;

ebx = temp;

 

Xchg al,al ; а эта команда не делает ничего

xchg ax, word ptr [si] ; обменять содержимое регистра ах и слова в памяти по адресу в [si].

 

Для условной пересылки данных используется команда:

CMOVcc <приёмник><источник>

 

Набор команд, которые копируют содержимое источника в приемник, если удовлетворяется то или иное условие.

Можно использовать команды CMOVcc сразу после команды СМР (сравнение) с теми же операндами, например:



Читайте также:

 

Строковые операции в ассемблере

Строковые операции называют цепочечными или операциями над цепочками байтов.

Строка-это цепочка байтов, для которой известен алгоритм определения ее длины.В языках программирования прописывают процедуру определения длины строки, а также вводят ограничение на то, какие байты могут содержаться в строке, а также в какой кодировке должны интерпретироваться строки- сколько байтов кодирует символ, и какая таблица будет использоваться для сопоставления байтов и символов.В языке Си под строкой понимается последовательность байтов, заканчивающаяся байтом со значением 0.

Строковые операции работают с одним элементом строки: байтом, словом, двойным словом.Для того чтобы команда манипулировала последовательностью таких элементов, используются команды-префиксы:

REP-повторять, пока содержимое ECX не обратится в 0

REPE/REPZ-повторять пока равно/нуль. Данный префикс заставляет выполнять строковую команду до тех пор, пока содержимое ECX не обратится в 0

REPNE/REPZ-повторять пока не равно/не нуль. Данный префикс заставляет выполнять строковую команду до тех пор, пока содержимое ECX не обратится в 0

Строковые команды считают, что строка -источник находится по адресу DS:ESI(для нас это просто ESI),а строка-приемникпо адреса ES:EDI(для нас просто EDI). Все строковые команды можно разделить на шесть групп:

1. Команды пересылки

MOVSB - копирование строки байтов.

MOVSW - копирование строки слов.

MOVSD - копирование строки двойных слов

Возможен также формат MOVS приемник, источник-в этом случае ассемблер по типу операндов сам определяет, какую из трех форм команд следует выбрать. Кобанда копирует элемент строки из адреса, определяемого ESI, в адрес, определяемый EDI. После выполнения такой команды содержимое ESI и EDI увеличивается(флаг DF=0) или уменьшается(флаг DF=1) на размерах элемента строки

2. Команды сравнения

CMPSB - сравнение строк байтов.

CMPSW - сравнение строк слов.

CMPSD - сравнение строк двойных слов

3. Команды поиска(сканирования)

SCASB - сканирование строки байтов.

SCASW - сканирование строки слов

SCASD - сканирование строки двойных слов

4. Команды чтения из строки

LODSB - чтение байта из строки.

LODSW - чтение слова из строки.

LODSD - чтение двойного слова из строки.

Возможен также формат LODS источник-в этом случае ассемблер по типу операндов сам определяет, какую из трех форм команд следует выбрать. Команда осуществляет копирование из памяти, которая адресуется ESI, элемента в AL,AX или EAX. После выполнения такой команды содержимое EDI увеличивается на размер элемента строки

5. Команда записи в строку

STOSB - запись байта в строку.

STOSW - запись слова в строку.

STOSD - запись двойного слова в строку.

Возможен также формат STOS приемник, источник-в этом случае ассемблер по типу операндов сам определяет, какую из трех форм команд следует выбрать. Команда осуществляет копирование в память, которая адресуется EDI, элемента из AL,AX или EAX. После выполнения такой команды содержимое EDI увеличивается на размер элемента строки

6. Команды чтения/записи строки из порта

При использовании префиксов (REP, REPZ/REPE, REPNZ/REPNE)действие команд распространяется на цепочки байтов.

Использование строковых команд

// string.cpp: определяет точку входа для консольного приложения.
//
 
#include "stdafx.h"
#include <windows.h>//необходим для работы DWORD
#include <stdio.h> //необходим для работы printf
#include <conio.h>//необходим для работы _getch()
/*Объявление функции*/
DWORD lens (char *);//функция определения длины строки
void cats(char*, char*,char*);//функция слияния двух строк в третью
/*Объявление переменных*/
char a[]="fdhfjliop";
char b[]="12345";
char c[]="4";
 
int _tmain(int argc, _TCHAR* argv[])
{
	__asm{
		/*мы собираемся вызывать процедуру cats,которая своими параметрами имеет 3 указателя
		на строку типа char(в порядке a,b,c).  Чтобы процедура cats могла получить параметры,
		передаемые по значению, мы должны поместить их в стек(в порядке c,b,a), а затем выровнять стек */
		LEA EAX,c;//помещаем в регистр EAX адрес первого элемента строки c
		PUSH EAX;//помещаем содержимое EAX в стек
		LEA EAX,b;//помещаем в регистр EAX адрес первого элемента строки b
		PUSH EAX;//помещаем содержимое EAX в стек
		LEA EAX,a;//помещаем в регистр EAX адрес первого элемента строки a
		PUSH EAX;//помещаем содержимое EAX в стек
		CALL cats;//вызываем процедуру
		ADD ESP,12;// выравниваем стек
	};
	printf("%s\n",c);//выводим суммарную строку
	_getch();
	return 0;
}
 
//функция определения длины строки
DWORD lens (char * s)
{
    DWORD l=0;
	__asm
    {
	    CLD;//задаем направление сканирования (сбрасываем флаг DF-флаг направления).
		//данный флаг учитывается в строковых операциях.Если
Учебные пособия по

AVR - Подпрограммы сборки

AVR Assembly поддерживает повторное использование кода с помощью подпрограмм. Подпрограммы - один из самых эффективных способов облегчить вашу жизнь, поскольку вы можете написать код, который делает то, что вы хотите, один раз и повторно использовать его снова и снова.

Однако, прежде чем научиться вызывать подпрограммы, вы должны понять концепцию The Stack Pointer .

Указатель стека

Указатель стека - это специальный регистр в памяти ввода-вывода, который указывает на пространство, выделенное в SRAM, называемое Стек .Стек используется для временного хранения значений регистров и возврата адресов при вызове подпрограмм.

Указатель стека - это 16-разрядный регистр, определенный во включаемых файлах как SPH и SPL. В микроконтроллерах с очень маленьким объемом SRAM SPH не требуется, используется только SPL.

Обычно стек начинается с конца SRAM и будет расти от более высоких до более низких значений адреса, когда в нем хранятся данные. Указатель стека всегда указывает на верхние стека.

Инициализация указателя стека

Чтобы использовать стек, указатель стека должен быть инициализирован адресом в SRAM. Поскольку указатель стека находится в памяти ввода-вывода, значения могут быть загружены в него с помощью инструкции out. На более новых AVR указатель стека будет инициализирован последним значением SRAM при включении питания, но на более старых он должен быть настроен вручную при запуске любой программы. Пример того, как это сделать, показан ниже

.
  ldi r16, НИЗКИЙ (RAMEND); загрузить младший байт RAMEND в r16
выход SPL, r16; сохранить r16 в нижнем указателе стека
ldi r16, ВЫСОКИЙ (RAMEND); загрузить старший байт RAMEND в r16
из SPH, r16; сохранить r16 в высоком указателе стека  

Константа RAMEND определена во включаемом файле как последний адрес в SRAM.Для многих микроконтроллеров RAMEND - это 16-битный адрес, поэтому он должен быть разбит на 8-битные компоненты с функциями HIGH и LOW для загрузки в рабочий регистр. На небольших микроконтроллерах RAMEND может быть меньше 16 бит, и в этом случае SPH не используется, а SPL - единственный регистр, который необходимо инициализировать.

  ldi r16, RAMEND; загрузить RAMEND в r16
выход SPL, r16; сохранить r16 в указателе стека  

Примечание: Хотя более новые микроконтроллеры автоматически инициализируют указатель стека RAMEND при включении питания, рекомендуется всегда инициализировать его в начале программы. Это защищает вас от указателя стека, начинающегося с неправильного места в случае программного сброса.

Хранение данных в стеке

После инициализации указателя стека регистры могут быть сохранены или загружены в стек - это называется , нажимая или выталкивая соответственно. Инструкции для этого приведены в таблице ниже.

Мнемоника Описание
нажимать push-регистр в стеке
население поп-регистр из стека

Инструкции push и pop просты в использовании:

  push r0; поместить r0 в стек
pop r0; восстановить r0 из стека  

Когда push вызывается с регистром, содержимое этого регистра сохраняется в верхних стека, т.е.е. адрес, загруженный в указатель стека. Указатель стека автоматически уменьшается на до при вызове push (помните, что стек растет от более высокого адреса к младшему).

Когда команда pop вызывается с регистром, в этот регистр загружается содержимое вершины стека. Указатель стека автоматически увеличивается на до при вызове pop.

Передача регистра в стек не стирает значение регистра, а просто копирует его содержимое в SRAM.Аналогично, , выталкивающее значение из стека, не стирает содержимое этого адреса в стеке.

При размещении нескольких регистров в стек, команды pop должны вызываться в порядке , обратном , чтобы восстановить значения в их исходные регистры, т.е.

  push r0; поместить содержимое r0 в стек
нажмите r1; поместить содержимое r1 в стек
нажмите r2; поместить содержимое r2 в стек

pop r2; восстановить содержимое r2
pop r1; восстановить содержимое r1
pop r0; восстановить содержимое r0  

Обратите на это особое внимание, так как вызов всплывающих инструкций в неправильном порядке приведет к восстановлению значений в неправильные регистры. Например

  ldi r16,0x01; загрузить r16 с помощью 0x01
ldi r17,0x02; загрузить r17 с помощью 0x02

нажимаем r16; сохранить r16 в стек
нажимаем r17; сохранить r17 в стек

pop r16; восстановить r16 (результат = 0x02)
pop r17; восстановить r17 (результат = 0x01)  

Приведенное выше приводит к тому, что содержимое r16 и r17 меняется местами, потому что инструкции pop не вызываются в порядке, обратном тому, в котором был вызван push (конечно, если вы действительно хотите поменять местами содержимое двух регистров без использования третьего, это отличный способ сделать это!).

Помните: Регистры должны быть извлечены из стека в порядке, обратном тому, в котором они были помещены, чтобы восстановить их исходные значения.

Теперь, когда у нас есть понимание указателя стека, мы можем перейти к подпрограммам.

Подпрограммы

Подпрограммы - это последовательности кода, которые можно повторно использовать в любой точке программы. Когда вызывается подпрограмма, микроконтроллер помещает адрес возврата в стек.Затем он перейдет к месту подпрограммы и выполнит код там. Когда достигается оператор возврата, адрес возврата будет , выскочит из стека , и микроконтроллер перейдет к инструкции сразу же после вызова подпрограммы.

Инструкции, используемые для вызова и возврата из подпрограмм, показаны ниже.

Мнемоника Описание
звонок длинный вызов подпрограммы
icall косвенный вызов подпрограммы
звонок относительный вызов подпрограммы
рет возврат из подпрограммы

Вызов подпрограмм

Ниже показан простой код, вызывающий подпрограмму.

  ldi r16,0x01; загрузить r16 с помощью 0x01
ldi r17,0x02; загрузить r17 с помощью 0x02

call addReg; подпрограмма вызова

цикл: цикл rjmp; бесконечная петля

addReg:
добавить r16, r17; добавить r16 и r17

ret; возврат из подпрограммы  

Вызов инструкции используется с меткой нашей подпрограммы addReg. Использование инструкции вызова заставит микроконтроллер перейти к заданной метке, выполнить там код и, когда будет достигнута инструкция ret, вернется к инструкции сразу после вызова - в этом случае бесконечный цикл, который мы установили с помощью rjmp.

Обратите внимание, что мы не можем явно передавать параметры в подпрограмму, как в случае с функциями в C. В приведенном выше примере подпрограмма ожидает, что ее параметры уже находятся в регистрах r16 и r17.

Подпрограммы

могут быть вызваны из всего программного пространства с помощью call или относительно на расстоянии до 4К слов с помощью rcall.

  rcall doSomething; вызов подпрограммы doSomething
...; другой программный код

сделай что-нибудь:
...; код подпрограммы
ret; возврат из подпрограммы  

Последний метод вызова подпрограмм, icall, является немного более продвинутым, чем это необходимо здесь, но для полноты он переходит к адресу подпрограммы, загруженной в указатель Z.

  ldi ZL, LOW (doSomething); адрес загрузки doSomething
ldi ZH, HIGH (делать что-то); в Z указатель

Я звоню				; косвенный вызов doSomething

сделай что-нибудь:
...; код подпрограммы
ret; возврат из подпрограммы  

Косвенные вызовы полезны, когда необходимо вызывать разные подпрограммы в зависимости от параметров времени выполнения. Обычно это реализуется с помощью таблицы поиска, а не с явной загрузкой параметров с помощью ldi, как показано выше.

Регистры сохранения

Когда вызывается подпрограмма, она может изменять регистры, которые вам понадобятся позже в программе.Чтобы этого не происходило, регистры можно поместить в стек в начале подпрограммы, а вернуть обратно в конце.

Например, если ваша подпрограмма изменяет r16 и r17, их значения можно сохранить, включив в подпрограмму следующее:

  rcall doSomething; подпрограмма вызова

...; остальная часть программы

сделай что-нибудь:
нажимаем r16; сохранить r16 в стек
нажимаем r17; сохранить r17 в стек

...; код подпрограммы

pop r17; восстановить r17 из стека
pop r16; восстановить r16 из стека

ret; возврат из подпрограммы  

Cortex-M3

ДОБАВИТЬ ADD R0, R1, Operand2
ADD R0, R1, 12 бит конст.

R0 = R1 + R2
N, Z, C, V S
АЦП АЦП R0, R1, R2
АЦП R0, R1, 8 бит, постоянная

R0 = R1 + R2 + C
N, Z, C, V S
ДОБАВИТЬ ADD R0, R1, 12 бит конст. 12 бит N, Z, C, V
ПОД SUB R0, R1, R2
SUB R0, R1, 12 бит конст.

R0 = R1 - R2
N, Z, C, V S
SBC SBC R0, R1, R2
SBC R0, R1, 8 бит, конст.

R0 = R1 - R2 - С
N, Z, C, V S
SUBW SUB R0, R1, 12 бит, конст. 12 бит N, Z, C, V
RSB RSB R0, R1, R2
RSB R0, R1, 8 бит, конст.
RSB R0, R1, R2, ASR № 23

.
R0 = R2 - R1 R0 = 8 бит const - R1
N, Z, C, V S
ADR ADR R0, этикетка +/- 12 бит
ADR.W R0, этикетка +/- 32 бит
.
LDR
STR
LDR R0, [R1, # 8bit const.]! -
LDR R0, [R1], # 8bit const.-
LDRB R0, [R1] -
STRB R0, [R1], № 1
/
.
B =, SB = ()
H =, SH = ()
LDR
STR
LDR R0, [R1, R2, {LSL # 0..3}]
STR R0, [R1, R2, {LSL # 0..3}]
.
B, SB, H, SH
LDR
LDRD
LDR R0, этикетка
LDRD R0, R1, этикетка
.
B, SB, H, SH
STR / STRD.
LDRT
STRT
.
LDR / STR.
LDRD
STRD
LDRD R0, R1, [R2, # 10bit const.]! -
LDRD R0, R1, [R2], # 10бит конст. -
LDRD R0, R1, [R2] -
STRD R0, R1, [R2]
/
.
4.
ЛДМ
СТМ
LDM R0, {R1-R3}
LDM R0 !, {R1-R3} - R0
IA, DB, FD, EA -.
/.
IA -
ДБ -.
PUSH
POP
НАЖАТЬ {R0, R2-R7, R12}
POP {R0, R2-R7, R12}
/
LDREX
STREX
LDREX R1, [R2, # 10бит конст.]
STREX R0, R1, [R2, # 10bit const.]
/.
B =, H =.
CLREX CLREX () .
BFC BFC R0, #lsb, # ширина
BFI BFI R0, R1, #lsb, # ширина R1 R0
UBFX UBFX R0, R1, #lsb, # ширина R1.R0
SBFX SBFX R0, R1, #lsb, # ширина R1. R0
UXTB
UXTH
UXTB R0, R1 {, ROR № 8,16,24}
SXTB
SXTH
SXTB R0, R1 {, ROR № 8,16,24}

8086 Руководство по сборщику для начинающих (часть 9)

8086 Учебник ассемблера для начинающих (часть 9)

Стек

Стек - это область памяти для хранения временных данных.Стек используется инструкцией CALL для сохранения адреса возврата для процедуры, RET Инструкция получает это значение из стека и возвращает к этому смещению. Совершенно то же самое происходит при вызове инструкции INT прерывание сохраняется в регистре флагов стека, сегменте кода и смещении. IRET инструкция используется для возврата из прерывания.

Мы также можем использовать стек для хранения любых других данных,
есть две инструкции, которые работают со стеком:

PUSH - сохраняет 16-битное значение в стеке.

POP - получает 16-битное значение из стека.

Синтаксис для инструкции PUSH :
PUSH REG 
PUSH SREG
PUSH memory
PUSH немедленный
РЕГ. : AX, BX, CX, DX, DI, SI, BP, SP.

SREG : DS, ES, SS, CS.

память : [BX], [BX + SI + 7], 16-битная переменная и т. Д.

немедленно : 5, -24, 3Fh, 10001101b и т. Д. ...


Синтаксис для инструкции POP :
POP REG 
POP SREG
Память POP
РЕГ. : AX, BX, CX, DX, DI, SI, BP, SP.

SREG : DS, ES, SS (кроме CS).

память : [BX], [BX + SI + 7], 16-битная переменная и т. Д.

Примечания:

  • PUSH и POP работают только с 16-битными значениями!
  • Примечание: НАЖМИТЕ немедленно работает только на процессорах 80186 и новее!

Стек использует алгоритм LIFO (Last In First Out),
это означает, что если мы помещаем эти значения одно за другим в стек:
1, 2, 3, 4, 5
первое значение, которое мы попадет на попу будет 5 , затем 4 , 3 , 2 , и только потом 1 .

Очень важно сделать равное количество PUSH s и POP s, иначе стек может быть поврежден и вернуть его будет невозможно к операционной системе. Как вы уже знаете, мы используем инструкцию RET для возврата к работе. система, поэтому при запуске программы в стеке есть адрес возврата (обычно это 0000ч).

инструкции PUSH и POP особенно полезны, потому что мы не иметь слишком много регистров для работы, поэтому вот трюк:

  • Сохранить исходное значение регистра в стеке (используя PUSH ).
  • Используйте реестр для любых целей.
  • Восстановить исходное значение регистра из стека (используя POP ).

Вот пример:
ORG 100ч

MOV AX, 1234h
PUSH AX; сохранить значение AX в стеке.

MOV AX, 5678h; измените значение AX.

POP AX; восстановить исходное значение AX.

RET

КОНЕЦ 

Другое использование стека - обмен значениями,
здесь пример:

ORG 100ч

MOV AX, 1212h; хранить 1212h в AX.MOV BX, 3434h; хранить 3434h в BX


PUSH AX; сохранить значение AX в стеке.
PUSH BX; сохранить значение BX в стеке.

POP AX; установите AX в исходное значение BX.
POP BX; установите BX в исходное значение AX.

RET

КОНЕЦ 

Обмен происходит потому, что стек использует алгоритм LIFO (Last In First Out), поэтому, когда мы нажимаем 1212h , а затем 3434h , при pop мы сначала получаем 3434h и только после него 1212х .

Область памяти стека задается регистром SS (сегмент стека) и SP (стек Указатель) зарегистрируйте. Обычно операционная система устанавливает значения этих регистров на запуск программы.

Инструкция « PUSH source » выполняет следующие действия:

  • Вычтите 2 из регистра SP .
  • Запишите значение источник по адресу SS: SP .

Инструкция « POP destination » выполняет следующие действия:
  • Записывает значение по адресу SS: SP в destination .
  • Добавить 2 в регистр SP .

Текущий адрес, на который указывает SS: SP , называется вершиной стека .
Оставить комментарий

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *