Указатель в программировании.
В статье MS-DOS и TASM 2.0. Часть 9. Указатель просто и понятно было рассмотрено, что такое указатель в программировании (pointer). Сейчас мы перейдём к вопросу практического использования указателя. Ещё раз напомним, что указатель в ассемблере — более широкое понятие, чем в Си и С++, где указатель определён как переменная, значением которой является адрес ячейки памяти. Указатель — не только переменная. Указатель в программировании на ассемблере — адрес определённой ячейки памяти. Жёсткой привязки к понятию «переменной» нет.
Преимущество указателя — простая возможность обращаться к определённой части исполняемого кода либо данных, избегая их дублирования. Например, один раз написав код функции, мы можем обращаться к нему неоднократно, осуществляя вызов указанной функции. Кстати, вызов функции — это переход исполнения кода по указателю, который для удобства «обозвали» понятным для человека названием (ну, например, «MyBestFunc»).
Указатель в программировании используется также для получения и передачи входных-выходных значений функций. С этим применением мы встретимся при Windows программировании.
Указатель — адрес ячейки памяти.
Указатель в программировании на ассемблере — адрес ячейки памяти, содержащей определённые последовательности цифр — блоки кода и данных. Блоки кода и данных называются значениями указателя, например: строка, массив, структура, функция, переменная, константа.
Указателю можно присвоить условное обозначение (const_a, const_b, my_mass_1, MY_STRUCT_1, BitMask, my_prnt_func), определив тип данных или кода, на которые он указывает (db, dd, STRUC, RECORD, proc). Практически мы уже проделывали эти операции, создавая исходники наших простейших программ:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | ... const_a db 5h const_b dd 15h ... my_mass_1 db 10 dup(8) ... MY_STRUCT_1 STRUC;В отличие от MASM - STRUC, а не STRUCT member_1 dw ?; member_2 db ?; MY_STRUCT_1 ENDS ... BitMask RECORD f0:4=1,f1:4=1,f2:4=0,f3:4=0 ... my_prnt_func proc PASCAL pMessage1:WORD, pMessage2:WORD local tmp:WORD mov ax,pMessage2 mov tmp,ax mov ah,09h mov dx,pMessage1 int 21h mov ah,09h mov dx,tmp int 21h ret my_prnt_func endp ... |
Работа с указателями.
Основная проблема начинающего программиста — это различие между указателем и значением, расположенным по адресу памяти, на которую указывает указатель.
Получение значения, на которое указывает указатель в Си и C++ называется «разыменование указателя». Это понятие можно употреблять и в ассемблере.
Получать адрес ячейки памяти (значение указателя) при программировании на ассемблере можно двумя способами, назовём их «статический» и «динамический». Реализовано это с помощью инструкций OFFSET и LEA.
При «статическом» способе указатель будет вычислен во время компиляции. Он не меняется и не вычисляется в процессе выполнения программы. В очень упрощённой форме: его значение равно смещению в байтах от начала программы, спроецированной в память, плюс адрес в оперативной памяти — «точка входа», адрес загрузки программы в память, который задаётся во время компиляции и записывается в заголовке (в начале) файла. Таким образом полученное значение фактически является константой.
OFFSET (Offset — смещение).
Получить «статическим способом» указатель в программировании на ассемблере можно с помощью оператора OFFSET (оператор — команда компилятору — подпрограмме, которая собирает исполняемый файл из исходного кода, написанного языком программирование). Offset возвращает значение метки в памяти. Меткой является любое именованное значение кода и данных. Например, имя переменной, константы или массива (именованное обозначение блока данных). Имя функции — фактически также является меткой (именованным обозначением блока кода).
LEA (Load Effective Address — загрузить эффективный адрес).
При «динамическом» способе адрес вычисляется в процессе исполнения программы. Для этого используется команда LEA (Load Effective Address).
lea операнд1, операнд2
Операнд 1 — это регистр-приёмник (ax, bx, dx и т.д.), куда будет перемещён эффективный адрес (указатель) ячейки памяти, в которой расположен операнд2.
Для начала, необходимо усвоить, что результат оба способа дают одинаковый, но иногда компилятор не имеет возможности определить указатель во время сборки программы. Например, при выделении динамической памяти (столкнёмся в 32 битном Windows программировании), не известно, по какому адресу она будет выделена. Если по этому адресу у нас будет находиться структура, то определить указатель на поля структуры возможно с использованием команды LEA.
Указатель в программировании на примере ассемблерного кода.
Ниже мы приводим код, который необходимо внимательно изучить. Даётся подробный комментарий. Не поленитесь и прогоните исполняемый файл GBLPTR.COM через отладчик Turbo Debuger — только так можно изучить указатель в программировании и познать Дао…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 | ;GBLPTR.ASM .model tiny ;for СОМ .code ;code segment start org 100h ;offset in memory = 100h (for COM) ;---------Примеры констант------------------------------------------- .data ;-------------------------------------------------------------- data_a db 3ah ;в части памяти, размером в БАЙТ (Byte), которую мы ;обозвали data_a находится шестнадцатеричное число 3ah ;(десятичное число 58) data_b dw 5ah ;в части памяти, размером в СЛОВО (World), которую мы ;обозвали data_b находится шестнадцатеричное число 5ah ;(десятичное число 90) data_c dd 7ah ;в части памяти, размером в ДВОЙНОЕ СЛОВО (World), ;которую мы обозвали data_с находится шестнадцатеричное ;число 7ah (десятичное число 122) data_d db 'This is data_d string $';в части памяти находится строка ;(часть байт,заключённых в кавычки),которую мы ;обозвали data_d my_mass_1 db 10 dup(8); создать байтовый массив, состоящий из 10 ;байт и заполнить его цифрами 8 (дублировать-DUPlicate ;10 раз число 8). MY_STRUCT_1 STRUC;В отличие от MASM - STRUC, а не STRUCT member_1 dw ? member_2 db ? MY_STRUCT_1 ENDS my_struct MY_STRUCT_1 <5> .code start: main proc ;-------------data_a db 3ah------------------------------------------ ;---------------Работаем со значением указателя---------------------- ;Работаем непосредственно с переменной data_a, размером ;в байт. Переменная определена в блоке ДАННЫХ: data_a db 3ah ;ah - одна из двух частей регистра AX. AX=AH+AL ("A High" и "A Low") ;ah - размером в байт, поэтому mov ah,data_a прекрасно работает. xor ah,ah;ah=0 mov ah,data_a;data_a db 3ah ;data_a - самый простой способ записи значения xor ah,ah;ah=0 mov ah,[data_a];data_a db 3ah ;[data_a] - то же, что и data_a. TASM всеяден. Различные способы ;обозначения данных (в квадратных скобках [...]и без них) ;употребляются в разных ассемблерах. Для TASM, FASM, MASM32, с ;которыми мыбудем работать приемлимо и то и другое. Выбирайте то, ;что удобнее для Вас. xor ah,ah;ah=0 mov ah,byte ptr[data_a];data_a db 3ah ;byte ptr[data_a] - byte pointer [значение] - читать нужно ;так: "берём значение размером в byte из ячейки памяти, на который ;указывает указатель data_a". Оно будет равно 3ah (data_a db 3ah) ;byte ptr - прямое указание компилятору на размер данных (byte), ;которые необходимо взять из ячейки памяти [data_a]. ;Если мы напишем mov ax,word ptr[data_a], то в ax попадёт байтик ;3ah (data_a db 3ah) и часть, в размере байта из слова (word) ;следующей ячейки памяти (data_b dw 5ah). xor ah,ah;ah=0 mov ah,byte ptr data_a;data_a db 3h ;byte ptr data_a - то же, что и byte ptr[data_a] ;---------------Работаем с указателем-------------------------------- ;---------------Получаем указатель в ax------------------------------ ;---------------offset----------------------------------------------- xor ax,ax;ax=0 mov ax,offset data_a;Правильно. В AX - указатель: ячейка в памяти, ;конкретное значение, в которой расположено ;число 3ah (data_a db 3ah) mov ah,data_a ;А вот так получим значение по указателю на ;data_a (data_a db 3ah) mov ax,word ptr data_a;Явно указали размер word ptr и ;получили значение в слово (байт data_ ;a + байт из значения data_b) - как ;уже говорилось выше ;xor ah,ah;ah=0 ;mov ah,offset data_a;Ошибка - указатель (адрес, смещение) в 16 ;битном ассемблере - DW, а AH - ;размером в байт (data_a DB 3ah) ;---------------lea-------------------------------------------------- xor ax,ax;ax=0 lea ax, data_a;То же, что и mov ax,offset data_a ;---------------offset []-------------------------------------------- xor ax,ax;ax=0 mov ax,offset [data_a];Правильно - для совместимости с другими ;ассемблерами ;---------------lea []----------------------------------------------- xor ax,ax;ax=0 lea ax, [data_a];То же, что и mov ax,offset data_a ;---------------offset byte ptr[]------------------------------------ xor ax,ax;ax=0 mov ax,offset byte ptr[data_a];Правильно - "указатель, который указывает на блок памяти, ;размером в байт, где расположено число 3ah (data_a db 3ah)" ;---------------lea byte ptr[]-------------------------------------- xor ax,ax;ax=0 lea ax, byte ptr[data_a];То же, что и mov ax,offset data_a ;---------------offset byte ptr-------------------------------------- xor ax,ax;ax=0 mov ax,offset byte ptr data_a;Правильно ;---------------lea byte ptr---------------------------------------- xor ax,ax;ax=0 lea ax, byte ptr data_a;То же, что и mov ax,offset data_a ;-------------------------------------------------------------------- xor bx,bx;bx=0 mov bx,ax;В ax - указатель на data_a mov di,bx ;Разыменовываем указатель (получаем значение по адресу памяти, куда ;указывает указатель) xor bx,bx ;mov bx,byte ptr [ax];Ошибка - bx - размером в DW ;mov bh,byte ptr [ax];Ошибка - ммм... В Win32 будет работать mov di,ax mov bh,byte ptr [di];Работает: bh=(byte ptr data_a)=3ah ;-Указатель на функцию, массив, структуру, строку-------------------- ;Массив,структура, строка, функция - фактически именованный указатель. ;Указатель на код аналогичен указателю на данные (data_a db 3ah), ;так как для процессора код и данные - только совокупность цифр. ;Поэтому, мы спококойно можем производить с именнованными указателями ;на части кода те же действия, которые мы производили с указателями ;на данные. ;---функция xor ax,ax;ax=0 lea ax, my_prnt_func; xor ax,ax;ax=0 mov ax, offset my_prnt_func; xor ax,ax;ax=0 lea ax, [my_prnt_func]; xor ax,ax;ax=0 mov ax, offset [my_prnt_func]; ;---массив xor ax,ax;ax=0 lea ax, my_mass_1; xor ax,ax;ax=0 mov ax, offset my_mass_1; xor ax,ax;ax=0 lea ax, [my_mass_1]; xor ax,ax;ax=0 mov ax, offset [my_mass_1]; ;---структура xor ax,ax;ax=0 lea ax, my_struct; xor ax,ax;ax=0 mov ax, offset my_struct; xor ax,ax;ax=0 lea ax, [my_struct]; xor ax,ax;ax=0 mov ax, offset [my_struct]; ;---строка xor ax,ax;ax=0 lea ax, data_d; xor ax,ax;ax=0 mov ax, offset data_d; xor ax,ax;ax=0 lea ax, [data_d]; xor ax,ax;ax=0 mov ax, offset [data_d] mov my_mass_1[0],1;поместить в первый байт число 1 (поля ;массива считаются с нуля, а не с единицы) mov my_mass_1[7],3;поместить в восьмой байт число 3 ;Cтроку можно рассматривать как одномерный ;массив байт ;data_d db 'This is data_d string $' mov data_d[0],'I' ;поместить в первый байт символ 'I' ;(поля массива считаются с нуля, ;а не с единицы) mov data_d[1],'t' ;поместить во второй байт символ 't' mov data_d[2],"'" ;поместить во второй байт символ ''' mov data_d[3],'s' ;поместить во второй байт символ 's' mov data_d[4],' ' ;поместить во второй байт символ ' ' mov data_d[5],'m' ;поместить во второй байт символ 'm' mov data_d[6],'y' ;поместить во второй байт символ 'y' ;Теперь: data_d db "It's my data string $" mov my_struct.MY_STRUCT_1.member_1,1; structure mov my_struct.MY_STRUCT_1.member_1,2; structure mov bx, offset my_struct mov [bx].member_1,7;structure ;---функция ;Обычный вызов call my_prnt_func PASCAL, offset InCode, offset data_d ;Вызовем функцию по указателю push offset data_d push offset InCode ;lea ax, my_prnt_func;Можно так. Так даже предпочтительней. mov ax, offset my_prnt_func call ax mov ax,4c00h int 21h main endp my_prnt_func proc PASCAL pMessage1:WORD, pMessage2:WORD local tmp:WORD mov ax,pMessage2 mov tmp,ax mov ah,09h mov dx,pMessage1 int 21h mov ah,09h mov dx,tmp int 21h ret my_prnt_func endp ;DATA InCode db 'InCode String : $' len = $ - InCode end star |
Сегментная адресация.
Вы уже заметили, что указатель на код и данные состоит из двух частей: пары сегмента и смещения, разделённых двоеточием. Это связано с так называемой сегментной адресацией, используемой микропроцессора 8088, на базе которого работают современные Intel и AMD процессоры. В этом микропроцессоре используется сегментная организация памяти.
Имеется четыре сегментных регистра, к которым микропроцессор имеет одновременный доступ. Базовые адреса сегментов находятся в сегментных регистрах:
- CS Регистр сегмента кода
- DS Регистр сегмента данных
- SS Регистр сегмента стека
- ES Регистр дополнительного сегмента
Для доступа к данным внутри сегмента обращение производится относительно начала сегмента линейно, т.е. начиная с 0 и заканчивая адресом, равным размеру сегмента. Для обращения к любому адресу в программе, компьютер складывает адрес в регистре сегмента и смещение. Например, первый байт в сегменте кодов имеет смещение 0, второй байт – 1 и так далее.
Физический адрес, то есть непосредственный указатель на область памяти, принято записывать парой этих значений, разделенных двоеточием СЕГМЕНТ:СМЕЩЕНИЕ, например:
сs:014А
сs:0151
сs:0153
…
Как неоднократно говорилось, программа типа *.COM представляет собой практически живой образ данных и машинных команд, предназначенных для выполнения процессором. Отображается в оперативную память без изменений. Начальный адрес загрузки в память — 0100h. Все сегменты имеют одно значение — работают с одним блоком памяти (в нашем примере он находится по адресу 4CEAh).
1 2 3 4 5 6 7 8 9 10 | Указатель на память|Значение указателя |Значение указателя (смещение) |(цифры для машины) |(код или данные для нас) -------------------|-------------------| сs:014F |8BF8 |mov di,ax сs:0151 |8A3D |mov bh,[di] сs:0153 |33C0 |xor ax,ax ... ds:0248 |24h |$ ds:0249 |00h | ds:024A |3Ah |: |
Указатель в программировании — одно из основополагающих понятий, уяснение которого является обязательным для написания кода с применением любого языка. К программированию в системе Windows переходите, только основательно проштудировав данный вопрос.