Дневники чайника
Дневники чайникаСтек.
Восьмой день
Стек — специально выделенная область памяти для передачи или сохранения данных.
Мы всё время наблюдаем такую форму записи (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 < jmp. &user32.MessageBoxA> ; \MessageBoxA 00401013 6A 00 push 0 ; /ExitCode = 0 00401015 E8 06000000 call < jmp.&kernel32.ExitProcess> ; \ExitProcess 0040101A FF25 08204000 jmp dword ptr ds:[< user32.MessageBo> ; user32.MessageBoxA 00401020 FF25 00204000 jmp dword ptr ds:[< kernel32.ExitPro> ; 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 операнд |
Действие | Толкает значение в стек |
Примечание | Если нужно толкнуть в стек все 8 регистров общего назначения (EAX, EBX, ECX, EDX, EBP, ESP, ESI, EDI), то лучше использовать одну команду PUSHA/PUSHAD |
Чтоб достать число с «верхушки стека» куда-то, пишите:
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:
.datamsg:
. 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
мы можем узнать, какие аргументы этот системный вызов принимает:
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:
.datamsg:
.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 выполнение системных вызовов осуществляется так:
.datamsg:
.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.
Пересылка данных
|
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; возврат из подпрограммы
ДОБАВИТЬ | 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 :РЕГ. : AX, BX, CX, DX, DI, SI, BP, SP.PUSH REG SREG : DS, ES, SS, CS. память : [BX], [BX + SI + 7], 16-битная переменная и т. Д. немедленно : 5, -24, 3Fh, 10001101b и т. Д. … |
Синтаксис для инструкции POP :РЕГ. : AX, BX, CX, DX, DI, SI, BP, SP.POP REG 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 , называется вершиной стека .