Міністерство освіти і науки України
Національний університет «Львівська політехніка»
Кафедра АСУ
Методичка №1
З курсу програмування в асемблері
Лабораторна робота №3
Тема – робота з пам’яттю.
Мета – навчитися використовувати системну пам’ять та розміщати в ній дані з файлу.
Львів 2010
ТЕОРЕТИЧНІ ПОЛОЖЕННЯ
Вступ
В Win32Api кожен об’єкт має свій унікальний ідентифікатор в системі. Для простих об’єктів він дорівнює його віртуальній адресі.
Цей ідентифікатор носить назву дескриптор або хендл (від. англ. HANDLE), і дуже часто використовується як параметр у функціях Win32Api (довжина 4 байти). Об’єкти котрі постійно фігурують в системі називаються атомами.
Хендли можуть ідентифікувати вікна, меню, блоки пам’яті, екземпляри програми, пристрої виводу, файли, аудіо та відео потоки, та інші об’єкти. Атоми ідентифікують стандартні іконки, курсори та об’єкти, які не змінюються при наступному завантаженні системи.
Більшість дескрипторів є значеннями індексів внутрішніх таблиць, які Windows використовує для доступу та керування своїми об’єктами. Звичайно, програми користувача (ужитки) в захищеному режимі не мають прав доступу до цих таблиць. Тому, коли необхідно отримати чи змінити дані, що пов’язані з певним об’єктом Windows, ужиток використовує відповідну функцію API з параметром хендла цього об’єкту. Таким чином Windows забезпечує захист своїх даних при роботі у багатозадачному режимі.
Пряма та віртуальна адреса
Кожна програма може отримати 4 Гб пам’яті для власних потреб, іменованою віртуальною сторінкою. Це смішно, парадокс, у комп’ютера, наприклад, встановлено плату на 256 Мб, а кожна програма може мати в своєму розпорядженні 4Гб. В кожну доль часу, скажемо мікросекунду, існують ділянки пам’яті котрі не використовуються, їх можна тимчасово зберегти на жорсткому диску, при умові якщо пам’яті не вистарчає місця, такі тимчасові файли називаються файлами підкачки (SWAP File). За допомогою файлів підкачки кожна програма може мати 4 ГБ у своєму розпорядженні. Віртуальна адреса, це адреса у просторі віртуальної сторінки, а пряма адреса це реальна адреса у пам’яті комп’ютера.
Виділення пам’яті
Пам’ять є основним ресурсом при програмуванні в багатозадачному середовищі. Множина вільних фрагментів пам’яті називаються ХІПИ (від англійського слова HEAP). Програміст може виділити для своєї програми блок пам’яті будь-якої довжини, що не перевищує загальний об’єм вільної пам’яті. В Windows пам’ять виділяється в 2 етапи:
спочатку система виділяє фрагмент віртуальної пам’яті, який отримує свій ХЕНДЛ, але не отримує реальної адреси (за допомогою функції GlobalAlloc)
потім система розміщує (блокує) цей фрагмент у реальній пам’яті і фрагмент отримує початкову адресу (за допомогою функції GlobalLoсk)
Після того, як програміст отримує адресу початку виділеного блоку, він може її використовувати. Комірки з адресами до початку та після кінця блоку використовувати не можна, тому що вони належать іншим програмам або системі.
Після того, як програма використала блок пам’яті, його необхідно розблокувати(за допомогою функції GlobalUnlock) Таким чином, він знову стає віртуальним, і при необхідності може бути переміщений системою в інше місце або на диск. Якщо програма довго не розблоковує блок пам’яті, то це негативно відображається на продуктивності операційної системи в цілому. Отже, якщо після розблокування пам’яті її знову заблокувати, адреса початку блоку може бути іншою. Якщо програміст взагалі відмовляється від використання виділеного блоку пам’яті, він повинен звільнити блок, що веде до знищення його хендла. Звільнення блоку пам’яті здійснюється за допомогою функції-підпрограми GlobalFree.
Поняття пам’ять та розташування в ній даних.
Секрет мови Асемблер відкривається при роботі з пам’яттю комп’ютера, оскільки для асемблера пам’ять це гігантський масив даних де розмір кожної комірки (елемента) рівний одному байту. Надалі порядковий номер елемента масиву пам’яті ми будемо називати адресою або зміщенням. Якщо вважати, що пам’ять це просто масив елементів то всі дані у пам’яті розміщуються послідовно, тобто розташовані один за іншим. Розглянемо на прикладі як це все виглядає :
Візьмемо невелику частину пам’яті, для прикладу, розмістимо в ній 4 змінні :
.Data ; сегмент глобальних даних
x dd 0
y dw 1023h;
z db ‘kernel32.’,0
k db 0
Представимо це схематично на Рис. 3.1.
Рис 3.1
Розпочнемо розшифровувати вище згаданий записи.
Почнемо з символу „х”, у пам’яті це такий „прапорець” котрий сигналізує початок змінної х. Запис „dd” означає що це зміна DWORD (подвійне машинне слово, рівне 4 байт), іншими словами це тривалість змінної, а символ „0”, означає що початково змінна х рівна нулю, тобто усі 4 байти рівні нулю. Фізично це виглядає так, система бачить що програміст хоче записати блок розміром 4 байт, виділяє “віртуальну рамку”, і як бачить так і пише з ліва на право, розміром в 4 байти, тобто 4 нулі. Зверніть увагу що „х” є „прапорцем”, тобто адресою початку змінної х, іншими словами символ „х” є константою, що містить значення адреси початку змінної х, тому коли ви пишете „mov x,10”, асемблер розуміє що потрібно по адресі „х”, тобто у зміну х занести значення 10.
Аналогічно зі змінною ”у”. Порівняно з попередньою змінною різниця полягає у тому що ми просимо виділити блок пам’яті у 2 байти (dw - WORD). Якщо представити собі “віртуальну рамку” і записати дані у пам'ять як ми це бачимо, то при читанні пам’яті будемо бачити першим байтом значення 23h, а другим 10h.
Відносно змінної “z”, в асемблері масив символів позначається як список байті. Якщо ми створимо змінну типу байт (db - Byte) і пропишемо в лапках або в апострофах послідовність символів, це буде ознакою для асемблера, що в пам’яті оголошено послідовність символів. Байти можна прописати посимвольно, розділяючи їх комами. Наприклад, ‘k’,’e’,’r’,’’n’,’e’,’l’,’3’,’2’,’.’,0. Нуль в кінці, це також символ, тобто його ANSI значення. Цей символ називають нуль-символом, він є ознакою кінця стрічки. Відносно розташування, “віртуальна рамка”, має розмір в байт (1 символ). Опишемо процедуру занесення стрічки у пам’ять. Беремо перший символ і заносимо у пам'ять, зміщуємося у перед, записуємо наступний символ, і тд. В результаті, якщо подивитися на пам’ять з ліва на право, розташована стрічка символів буде виглядати навпаки. Це показано на Рис.3.1.
Відповідно змінна “k” розташовується ідентично до інших змінних.
Треба пам’ятати, що у пам’яті не грає ролі якого типу дані, вони просто існують.
Кожна програма у пам’яті розміщує власні дані конкретного типу в специфічних блоках, такі логічні блоки називаються сегментами, наприклад, сегмент даних використовується для глобальних даних, сегмент коду, використовується для розміщення інструкцій виконання, іншими словами коду, сегмент стеку для тимчасових даних, і т.д.
Приведення до типу
Іноді, бувають ситуації в яких для роботи з змінними потрібно вміти привести один тип до іншого. Привести можна з будь якого в будь який тип, наприклад:
Local x : DWORD
…..
mov ax , word ptr x
де „word ptr” це ознака приведення змінної х (4 байта ) до значення word (2 байта). Ми не можемо "втиснути" фізично зміну х (4 байти) в регістр ах (2 байти) цілком, а беремо з змінної х молодшу частину, тобто перших 2 байти.
Приведемо інший приклад, нехай у нас є вказівник на масив символів (string, тип з мови Pascal ). Адреса масиву знаходитися у регістрі edi. Завдання, потрібно занести у п’ятий символ масиву , букву „а”.
mov byte ptr [ edi + 4] , ’a’
Квадратні дужки означають, що потрібно занести (взяти) щось по адресі, що лежить регістрі поміж дужок. Квадратні дужки використовуються тільки для регістрів. В 32-бітному програмуванні тип регістрів поміж дужок, завжди мають бути розширеними (наприклад, eax,ebx,ecx,edx,…).
Чому ми приводимо до типу байт, розмір одного символу рівний одному байту. Пам’ятаємо що рахунок елементів починається з нуля, тобто якщо нам потрібен 5 елемент, він буде знаходитися в масиві у четвертій (5-1) позиції.
Інший приклад, у нас є адреса структури, приклад на основі попередньої лабораторної (типу SYSTEMTIME), нам потрібно занести в поле „wDay” значення 25. Адреса знаходиться у регістрі edi.
mov (SYSTEMTIME ptr [ edi ]). wDay , 25
Потрібно пам’ятати, що пам’ять це послідовна сукупність байт, тому до будь якого елемента структури можна добратися за допомогою звичайних методів приведення до типу. Спеціальні приведення використовуються тільки для зрозумілості і простоти. Наступний приклад, показує як за допомогою простих приведень до типу змінити значення поля „wDay” в структурі SYSTEMTIME. Адреса знаходиться у регістрі edi.
Ми знаємо вміст структури, прорахувати на скільки байт нам потрібно зміститися від початку змінної можливо. Елемент „wDay” є 4 в списку структури, кожен елемент структури рівний 2 байтом (тип WORD), тому нам потрібно зміститись на (4-1)*2=6 байтів.
mov word ptr [ edi+6] , 25
Взяття адреси (зміщення, посилання)
Навіщо ми беремо адресу, і що це таке, попробуємо зрозуміти на прикладі.
.data
x dd 0
z db 4 dup(0)
Вище написаний код означає, що в сегменті даних (в розділі глобальних даних) ми розмістили такі комірки(змінні):
"х" розміром 4 байти з значенням по-замовченню „0”.
"z" масив з 4-ох байтів. Елементи масиву по-замовченню рівні „0”.
Яка різниця поміж тими двома змінними з точки зору розташування у пам’яті. Ніякої. Змінна х та z мають розмір 4 байти. Байти розміщені послідовно. Але по звертанню до них, є дуже велика різниця. Змінна х розглядається як одна суцільна змінна, а z як сукупність байтів (масив), тобто добратися до 3 байта змінної х одною стандартною командою (інструкцією) неможливо ( x[3] неправильно), а тільки обхідними командами, через накладання маски і таке інше. Для змінної z, цих проблем не існує (z[3] правильно), оскільки "z" ми визначили як масив.
Поглянемо на цю проблему з іншої сторони, нехай у нас є адреса у пам’яті, котра вказує на змінну х. Ми знаємо що всі елементи змінних лежать в пам’яті послідовно, один за іншим. Якщо ми маємо адресу першого байту змінної "х", та змістимось від цієї адреси ще на 2 байти у перед, то доберемось до 3 байта змінної х. Таким чином можна працювати зі "суцільною" змінною як з масивом.
Постало питання, як взяти адресу змінної. Для цього існують інструкція lea, та оператор offset. Інструкція lea використовується для взяття адреси з локальних змінних, працює також для глобальних змінних, оператор offset використовується тільки для глобальних змінних. Наведемо приклад:
Дані знаходяться у сегменті даних (глобальні змінні)
.data
x dd 0
y db 4 dup(0)
.code
Запишемо у 3 байт, змінної х, значення 45
mov edi , offset x ; в edi знаходиться адреса змінної х
mov byte ptr [edi +2] , 45 ; рахунок здійснюється від нуля, не забуваємо привести до типу byte.
; в принципі можна і так:
mov byte ptr х+2 , 45 ;х, сам пособі є іменованою константою що містить в собі реальну адресу комірки х, тому можна її використовувати як адресу. З точки зору оптимізації, частіше використовують перший варіант звертання до пам’яті, але не і забороняють другий.
Попробуємо записати у 2 елемент масиву "у", число 10. Не забуваємо, що дані розташовані послідовно, тобто можна через адресу на змінну "х", добратися до елементів масиву "у".
mov byte ptr [edi +5] , 10 ; зміщення +4 буде вказувати на 1-й елемент масиву "у", зміщення +1, 2-й елемент масиву "у".
або:
mov byte ptr х+5 , 10
або:
mov byte ptr у+1 , 10 ; безпосередньо використовуючи мітку "у"
Дані знаходяться у сегменті стеку (локальні змінні)
……
LOCAL x : DWORD
LOCAL y[ 4 ] : byte
…….
Lea edi , x ;беремо реальну адресу
Подібно по попереднього прикладу, робота з пам’яттю ідентична, визначення її дещо інша (за допомогою інструкції Lea).
ПОРЯДОК ВИКОНАННЯ РОБОТИ
Запустити подану нижче програму, так як описано в попередній інструкції.
Розглянути текст програми, знайти функції виділення, блокування, розблокування та звільнення пам’яті.
Доповнити подану програму функцією вибору файлу GetOpenFileName.
Доповнити отриману програму фрагментом для визначення часу створення та часу останньої модифікації файлу за допомогою функції GetFileTime.
Перетворити час у зручний для сприйняття вигляд за допомогою функції FileTimeToSystemTime та вивести його за допомогою програми попередньої лабораторної роботи. Структуру даних часу типу SYSTEMTIME розмістити у виділеному блоці пам’яті.
Після закриття файлу виконати його запуск через оболонку WINDOWS за допомогою команди ShellExecute.
Кожний варіант програми оформити в звіті і відповісти на контрольні запитання.
Текст програми
Lab_3.inc
include WINDOWS.inc ; файл констант
include user32.inc ; файл заголовківl user32.dll
include kernel32.inc ; файл заголовківl kernel32.dll
includelib user32.lib ;файл бібліотеки з таблицею імпорту user32.dll
includelib kernel32.lib ;файл бібліотеки з таблицею імпорту kernel32.dll
MEMSIZE equ 1000000h ; 16 Mb ; власна константа рівна значенню 16*220
.data ;сегмент даних
title1 db 'Лабораторна робота №3',0 ;стрічка, використовуємо її для заголовку повідомлення
openname db 'lab_3.asm',0 ; стрічка, вміст якої шлях до файлу, по замовчуванню файл знаходиться у поточному каталозі
lab_3.asm
.386
.model flat,STDCALL
option casemap :none ;case sensitive
include lab_3.inc ; підключаємо файл заголовків
.code ; сегмент коду
Begin: ; мітка початку
call main ; виклик підпрограми main
invoke ExitProcess,NULL ; вихід з процедури
; власна процедура виділення пам’яті
Mem_Alloc PROC Buf_Size:DWORD
add Buf_Size,4 ; виділяємо буфер на 4 байти більший
invoke GlobalAlloc,GMEM_MOVEABLE or MEM_ZEROINIT,Buf_Size ; виділяємо область у динамічній пам’яті
push eax ; зберігаємо хендл області виділеного буфера у стек
invoke GlobalLock,eax ;фіксуємо у реальній
pop [eax] ;Витягнемо зі стеку значення і запишемо у перші 4 байта виділеного масиву,
;це значення є хенд області виділеної області
;наступні Buf_Size байт використовуємо як масив
add eax,4 ;зсуваємо вказівних початку масиву на 4 байти, оскільки перші 4 байти
;заняті значенням хендлу буфера
ret ; виходимо з процедури
Mem_Alloc endp
; власна процедура знищення пам’яті.
Mem_Free PROC DATA:DWORD
mov eax,DATA ;копіюємо значення параметра процедури у регістр eax, параметром є
;початкова (робоча) адреса буфера ( початок + 4 байти)
sub eax,4 ;пам'ятаємо, що перших 4 байти масиву - це хендл буфера
push [eax] ;беремо чотирьох байтне значення по адресі, що лежить у регістрі eax, заносимо його у стек
invoke GlobalUnlock,eax ;розблоковуємо пам'ять (всю розміром Buf_Size+4)
call GlobalFree ;оскільки параметр вже у стеці, викликаємо процедуру без параметрів, за дапомогою інструкції
;call (знищуємо хендел буфера)
ret ;вихід з процедури
Mem_Free endp
main proc ;основна процедура
LOCAL hFile,SizeRead:DWORD ; локальні змінна
LOCAL mem:DWORD
invoke CreateFile,addr openname,GENERIC_READ,\ ;відкриваємо файл
FILE_SHARE_READ,NULL,OPEN_ALWAYS,\
FILE_ATTRIBUTE_NORMAL,NULL
mov hFile,eax ;зберігаємо хенд відкритого файлу
invoke Mem_Alloc,MEMSIZE ; виділяємо пам’ять розміром значення MEMSIZE = 16 Мb
mov mem,eax ; зберігаємо вказівник на пам’ять у змінну mem
invoke ReadFile,hFile,mem,MEMSIZE,addr SizeRead,NULL ; читаємо файл і записуємо прочитане у виділену пам’ять
invoke CloseHandle,hFile ; закриваємо файл
invoke MessageBox,NULL,mem,offset title1,MB_OK ; виводимо що прочитали
invoke Mem_Free,mem ; звільняємо пам’ять
ret ; вихід з процедури
main endp
end Begin ;кінець програми
КОНТРОЛЬНІ ЗАПИТАННЯ
Що таке реальна і віртуальна пам’ять ?
Як зарезервувати фрагмент віртуальної пам’яті ?
Чи можна виділити блок реальної пам’яті, якщо не резервувати віртуальну пам’ять ?
Як виділити реальну пам’ять ?
Як перемістити блок виділеної пам’яті ?
Як звільнити блок пам’яті ?
Які ви знаєте функції для роботи з файлами ?
Як створюються файли ?
Що означає “відкрити файл через оболонку” ?
Як працює функція GetOpenFileName ?
Як працюють функції GetFileTime та FileTimeToSystemTime ?
Завдання
Використати функцію GetOpenFileName для вибору файлу. Зчитати вміст файлу у пам'ять. Підняти всі символи тексту у верхній регістр. Вивести файл на екран.
Вивести інформацію про операційну систему. Структуру розмістити у виділеній пам’яті. Використати функцію GetSystemInfo.
Створити файл структур. Наприклад, формату:
Ім’я
Фамілія
Вік
Курс
Інститут
Мінімальна кількість записів = 10. Тип полів структури - стрічка.
Після створення вивести вміст структур за допомогою функції MessageBox.
Використати функцію GetOpenFileName для вибору файлу. Перевірити, якщо вік файлу не перевищує 3 дні, виконати його. В протилежному випадку вивести діалогове вікно з питанням о видалені файлу. Якщо коритувач згодиться, витерти.
Вивести інформацію про операційну пам’ять. Структуру розмістити у виділеній пам’яті. Використати функцію GlobalMemoryStatus.
Вивести інформацію про BMP файл.
Примітка: перших 14 байт це заголовок файлу, наступних 40 байт інформація про BMP. Ця інформація являється структурою BITMAPINFO.
Використати функцію GetOpenFileName для вибору файлу. Зчитати зміст файлу у пам'ять та перетворити її у двійково-символьні послідовності. Створити у вибраній (за допомогою функції GetOpenFileName) директорії файл і записати результат.
Під розумінням "перетворити її у двійково-символьні послідовності" мається наступне, перетворити кожний байт файлу у двійкову символьну послідовність, наприклад, число 49 = "00110001", ітд. Примітка: вихідний файл має бути у 8 разів більшим від вхідного.
Записати у файл список імен файлів зі заданої директорії.
Для вибору файлу використати функцію GetOpenFileName. Зчитати зміст файлу у пам'ять та зробити реверс. Результат записати у новий файл.
Створити файл в якому розмістити послідовність Фібоначі. Елементи розділити комами. Кількість ітерацій рівна 50.
Перетворити вміст файлу у шістнадцятковий текс. Файл вибрати за допомогою функції GetOpenFileName. Тобто буква "А" = Ansi(65) =$41,і тд.
Використати функцію GetOpenFileName для вибору файлу. Вивести вікно повідомлення з двома кнопками (Yes, No), інформацією про вибраний файл (назва, шлях) та питанням: "Виконати Файл?". При натисненні кнопки " Yes " виконати файл за допомогою функції ShellExecute.
Для вибору файлів використати функцію GetOpenFileName. Об’єднати вміст всіх файлів в один результуючий файл.
d