Что такое стек? Как он работает?
Стек – это память для хранения адресов возврата. Обычно для его объяснения, приводят пример с книжной полкой. По сути, так и есть, и даже проще. Стек может быть реализован программно или аппаратно, что быстрее. У данного МК, он реализован аппаратно, имеет разрядность 13 бит, и 8 уровней вложенности. Это значит, что функция может вызвать сама себя 8 раз. На 9 – новый адрес – запишется на место 1, и после этого ваша программа зациклится, потому что стек будет переполнен, а адрес возврата утерян.
Чтобы разобраться, как он работает, и что делают команды call и return, наберите эту небольшую программу.
;---------------------------------- ;
#include<p17С756А.inc>
;---------------------------------- ;
org 0x00
goto main
org 0x05
;---------------------------------- ;
; Функции программы
;---------------------------------- ;
Call_1:
nop
nop
return
;--------------------------------- ;
Call_2:
nop
nop
return
;---------------------------------- ;
main:
call Call_1 ;
call Call_2 ;
goto main
END
В пункте меню View, активируйте подпункт Hardware Stack (рис.4.2).
Рис.4.2 - Стек
Запустите программу в отладчике, используя ручной режим. Нажимая клавишу <F7>, последовательно доберитесь до строки:
call Call_1 ; вызов функции Call_1
Ниже представлена упрощённая версия, файла листинга программы, созданного компилятором. Просто взгляните, а потом продолжите чтение.
Address:
0000 org 0x00
0000 goto main
0005 org 0x05
;--------------------
0005 Call_1:
0005 nop
0006 nop
0007 return
;--------------------
0008 Call_2:
0008 nop
0009 nop
000A return
;--------------------
000B main:
000B call Call_1 ;
000C call Call_2 ;
000D goto main
END
Имя функций Call_1 указывает на адрес 0х05, по которому размещена первая команда в памяти программ – nop. За ней ещё одна в ячейке памяти под номером - 0х06, и оканчивается функция командой возврата return, расположившейся в ячейке - 0х07.
0005 Call_1:
0005 nop
0006 nop
0007 return
Вспомните, когда мы рассматривали метки, упоминалось, что они являются средством организации программы на уровне пользователя. Как видите, так и есть, потому, что по адресу 0х05 располагается команда nop.
Теперь взгляните на адрес 0х0B (D‘11’), с которого начинается метка ‘main’, и следует команда вызова функции Call_1.
000B main:
000B call Call_1 ;
000C call Call_2 ;
000D goto main
END
При выполнении команды call – произойдёт следующее: текущий адрес 0x0B, на который указывает счётчик команд - PC, будет увеличен на 1 – PC + 1, то есть: 0x0B + 0x01 = 0x0C, после чего, тем самым начнёт указывать на следующую команду в памяти программ, и будет загружен в вершину стека (рис.4.3.). Произойдёт неявное выполнение команды goto на адрес 0х05, с которым ассоциирована метка подпрограммы Call_1.
Рис.4.3 - Стек после вызова функции
Когда все команды в теле функции Call_1 будут выполнены, и дело дойдёт до команды возврата return, произойдёт обращение к стеку, и выполниться переход по адресу, в нашем случае - это 0х0C, на который обращён указатель, и который был загружен в вершину стека командой вызова функции call. Это тем самым вернёт управление основной программе, и начнёт выполнение следующей команды, расположенной по адресу 0х0С. Если подобное описание автора показалось вам сложным, обратитесь к описанию системы команд, где данная информация представлена в более сжатой форме.
Найдите время, чтобы поэкспериментировать с кодом. Попробуйте вызвать функцию из функции несколько раз, и пронаблюдайте, что происходит с программой, и как заполняется стек. Добавьте в программу простейший макрос, откомпилируйте и посмотрите, как он располагается в памяти.
Соглашения об именовании
Выполнив уже две лабораторные работы, пришло время ознакомиться, с некоторыми правилами об именовании переменных и остальных членов программ. Имя – это уникальный идентификатор объекта, характеризующий его основные качества. Как было сказано в лабораторной работе № 2 – ячейка памяти, которая с чем-то ассоциирована, считается переменной. Чем больше информации способно нести имя, тем понятнее назначение члена в программе. Для удобства, автор использует один и тот же стиль во всех примерах, когда наделяет именами функции, метки и переменные. Если вы уже имели опыт программирования, и выработали для себя определённый стиль, тогда, конечно же, вам следует придерживаться его. В целях повышения читабельности кода, ознакомьтесь с соглашениями об именовании членов программ.
Переменные
Имена переменных обычно начинаются со строчной буквы. Это правило может быть нарушено, когда в программе используются копии реальных имён логического выражения, для повышения читабельности.
value EQU 0x20 ;
array EQU 0x21 ;
X1 EQU 0x22 ;
X2 EQU 0x23 ;
X3 EQU 0x24 ;
Переменные, которые состоят из нескольких слов – оба пишутся с маленькой буквы, и разделены знаком нижнего пробельного символа ‘_’. Если имена слишком длинные, сокращение происходит по согласным. В противном случае, слова остаются в том же виде. Также это правило нарушается для логических выражений.
arr_size EQU 0x21 ;
n_X1 EQU 0x22 ; инвертированный Х1
Функции и Макросы
Одиночные имена функций состоят из одного слова с заглавной буквы.
Calc: ;
return
Составные имена – оба слова начинаются с заглавных букв, разделенных знаком нижнего пробельного символа ‘_’. В случае длинного названия, сокращение идёт по согласным.
Calc_Array: ;
.
.
return
Init_Vars: ;
.
.
return
Strt_Tmr1: ;
ENDM
Метки
Главная метка программы ‘main’ всегда начинается со строчной.
main:
.
.
END
Промежуточные метки программы, состоят из одного либо двух литеральных символов, написанных с большой буквы.
main:
A2:
goto A2 ;
.
.
.
AT2:
goto AT2
END
Команды
Все команды состоят из строчных букв, если только имя не используется в заголовке. Тогда все буквы команды заглавные.
movwl D‘10’ ;
bsf value,2 ;
Директивы
Директивы #include, #define, а также имена подключаемых файлов состоят из строчных букв. Остальные состоят только из заглавных.
#include<source_file> ;
#define true 0x01 ;
.
.
#include”source_file”
#define false 0x00 ;
.
.
BANKSEL label ;
.
.
label EQU address ;