Перші кроки в низкоуровневом програмуванні

Перші кроки в низкоуровневом програмуванні

Могутня процедура Move

Move - цікава стандартна процедура, що дісталася нам у спадок ще від старого, доброго Turbo Pascal'я. Вона, напевно, з якоїсь помилково потрапила в мову програмування високого рівня, однак неабияк додала потужності і спростила життя (а ми тільки й раді). Начебто нічого особливого: procedure Move (const Source; var Dest; Count: Integer); Переміщує з Source в Dest ділянку пам'яті, рівний Count байтам. Причому, як ви помітили (якщо помітили), Source / Dest - НЕ покажчики, а безпосередньо змінні, і за точку відліку Move бере перший байт, яку він обіймав змінної. Подивіться на приклади: var
b. array [0..80] of byte;
c. array [4..67] of char;
w. array [8..670] of word;
procedure SomeProc;
begin
MessageBox (0, 'hello!', Nil, 0);
end;
begin
move (b, c [4], 20); // копіює 20 байт з початку масиву b в масив с, починаючи з 4-ої позиції.
move (b, w, SizeOf (b)); // копіює весь масив b в початок масиву w (зверніть увагу на невідповідність розмірів типів!)
move (w [4], w [50], SizeOf (word) * 100); // "відсуває" 100 значень з 4-ої на 50-у позицію всередині одного масиву (зверніть увагу - ділянки пам'яті Source і Dest перекриваються!)
move (SomeProc, b, SizeOf (b)); // копіює код процедури SomeProc в масив b
.

Теоретично, просто побайтно копіюються шматки пам'яті, незважаючи на типи даних і їх розміри. Чудово також те, що процедура коректно працює в разі, коли дані на новому місці "зачіпають" вихідні. У прикладах ми визначаємо розмір масиву за допомогою функції SizeOf. Вона повертає розмір в байтах або структури даних, або типу даних. Зауважу, що в реалізації Move не передбачено ніякої перевірки на вихід за межі змінних, що є приводом як для більшої обережності при її використанні, так і для всіляких низькорівневих трюків. Четвертий приклад якраз з таких. Він сьогодні для нас є ключовим, тож.

четвертий приклад

Логічно, завантажена в пам'ять і працює програма складається з коду - інструкцій для процесора ( "дієслів"), і даних, які цими інструкціями обробляються ( "іменників"). Зберігаються в пам'яті комп'ютера і ті, і інші одним способом - як послідовності байтів. І тому код іноді можна розглядати як дані. Будь я професором, сказав би, що в такому випадку життя проводиться "принцип фон-Неймана". У різних специфічних програмах (компіляторах, пакувальником виконуваних файлів, всіляких запобіжні заходи й т.д.) без нього не обійтися. Подібні методи справедливо вважаються прерогативою мов низького рівня (в першу чергу, асемблера), однак і з Delphi можна провести деякі цікаві експерименти такого сорту.

Тепер, ближче до нашого прикладу. Масив b заповниться поданням процедури SomeProc в пам'яті комп'ютера. Причому, нам не відомо, скільки байт вона займає - швидше за все, виходячи з її мініатюрності, в кінці масиву буде сміття - шматок або іншої процедури, яких даних. Але будь процедура побільше, то могла б і не поміститися в відведені 80 байт. Як бачите: суцільна невідомість, але ми спробуємо пролити на ситуацію трохи світла. Перше, що спадає на думку - подивитися, що ж таки записалося в наш масив. Цілком резонно! Ви, напевно, вже написали щось на зразок for i: = Low (b) to High (b) do
Memo1.Lines.Add (IntToStr (i) + '-' + IntToStr (b [i])) ;. і дивіться нерозуміючим поглядом на колонки чисел.

Ті, хто не написав, скоріше набирайте, а я ж не втрачу можливості дещо пояснити. Паскаль дає можливість задавати будь-які межі індексації масивів (що є ознакою "високорівневих" мови - наприклад, в мові Сі все масиви індексуються з нуля). У Delphi пішли далі і ввели функції Low і High, які визначають нижній і верхній індекси масиву, відповідно. Це дійсно зручно, так що рекомендую повсюдно в ваших програмах використовувати їх, позбавляючись від малозначних констант.

Отже, колонка чисел. Нічого, начебто, цікавого. Але я пропоную виводити не тільки код, але ще і символьне уявлення байта. У підсумку, пишемо наступне: for i: = Low (b) to High (b) do
Memo1.Lines.Add (IntToStr (i) + '-' + IntToHex (b [i], 2) + '' + chr (b [i])); Зверніть увагу - тепер використовується функція IntToHex замість IntToStr. Вона переводить число в його строкове представлення в шістнадцятковій системі числення. Першим аргументом подається саме число, другим - потрібне нам кількість знаків в шуканому поданні. Професійні програмісти дуже люблять використовувати шестнадцатеричную систему числення. По-перше, в ній для опису значення одного байта досить тільки 2-х символів. А по-друге, байти мають звичай групуватися в слова (2 байта), і подвійні слова (4 байти), для можливості подання чисел, великих 255. І тепер, перед нами стоїть завдання: визначити, яке число визначає машинне слово, що складається з 2-x байтів (порядок має значення!). Потрібно обчислювати: 28 * 256 + 86 = 7254. А ось якщо використовувати 16- чную (не хочу вже повторювати цей "дліннючее" слово) систему, то шукане число вийде простим "склеюванням". Тобто в даному випадку = 1C56! Ще виразніше її переваги проявляються, коли число треба навпаки, "розчленувати" на байти.

Отже, знову повертаємося до наших бананам. Запустіть програму і уважно вивчіть отриманий результат. В байтах з 14-го по 19-ий (висловлювався в 16-чной, вибачте-с) розташувалася наша рядок 'hello!'. Уже тепліше. Причому, зверніть увагу, вона завершується байтом з кодом нуль, так званим "нуль-термінатором". Так влаштовані рядки в Сі-програмах. Компілятор побачив, що викликається функція (MessageBox), аргументом якої є рядок в стилі Сі (в термінології Delphi їй відповідає тип PChar). І, тому, замість досить складною у внутрішньому поданні "паскалевской", сформував потрібну "сішную".

Йдемо далі, копаємо глибше. Як же все-таки визначити, скільки байт займає наша SomeProc? Для цього її потрібно трохи змінити: procedure SomeProc;
begin
asm
nop
nop
end;
MessageBox (0, 'hello!', Nil, 0);
asm
nop
nop
end;
end;

У Delphi є зручна можливість вставляти в код нашої програми шматки, цілком написані на асемблері. Інструкції записуються між ключовими словами asm і end і вставляються компілятором в потрібне місце об'єктного коду без змін. Нам знадобилася всього одна асемблерна інструкція - nop (абревіатура від "No OPeration"), головне завдання якої - нічого не робити, а спокійнісінько займати собою один байт пам'яті (скажу вам по секрету, з кодом 90.). Дехто вже, напевно , здогадався, що до чого. ми поставили цю інструкцію двічі в початок і в кінець нашої процидурку. Тепер, ми точно зможемо сказати, що все, що розташоване між двома парами байт з кодом 90 і є наша SomeProc! По дві же інструкції - для надійності, щоб відокремити свої nop'и від чужих. Звичайно, компілятору немає потреби просто так вст влять в код нічого не робить інструкцію, однак ще більш немає потреби вставляти її двічі.

Отже, барабанний дріб, запускаємо нашу програму. Так Так. Два рази по дев'яносто на самому початку - значить, ми потрапили, куди треба. Вже добре. Йдемо далі. Під номером, або як кажуть програмісти, щодо зміщення 1B бачимо другу пару, отже, знайдено, наша процедура займає 19 байтів! Тільки, що за чортівня? Рядки 'hello!' в цих межах немає! Але ж рядок-то знаходиться всередині процедури, чому ж в підсумку вона виявилася зовні? Ось вона, як і годиться, зсунулася на чотири байти щодо попередньої позиції (ми ж додали в загальній складності 4 nop'a), але вона зовні! Звідси варто зробити важливий висновок: незважаючи на те, що у вихідному коді програми дані і інструкції знаходяться поруч, в виконуваної програмі вони рознесені, кожен на своє місце (мухи окремо, котлети, самі розумієте.).

Тепер, давайте проведемо експеримент: не дозволимо компілятору самовільно виділяти місце під рядок, а зробимо це самі і подивимося, що станеться. SomeProc стане такою: Procedure SomeProc;
const
s: array [0..7] of char = 'hello!' # 0;
begin
asm
nop
nop
end;
MessageBox (0, s, nil, 0);
asm
nop
nop
end;
end;

Запускаємо програму, і що ми бачимо? Рядок зникла з насидженого місця в невідомому напрямку! Робимо ще один висновок: Дані зберігаються в різних місцях, і місця ці залежать від того, хто помістив туди їх - компілятор, або сам програміст. Розмови розмовами, а цілий рядок кудись поділася серед білого дня. Її неодмінно треба знайти!

В пошуках Немо

Перші кроки в низкоуровневом програмуванні

З'явилася таблиця з трьох колонок, яка чимось нагадує наші з вами недавно вивчаються. Дані представлені в середньої і в правій - як шістнадцяткові коди, і як символи, відповідно. А в лівій колонці показані зміщення.

Натискаємо Alt + G, щоб перейти до потрібного зміщення. Отже, вводимо перше - 00450C28, і відразу ж - удача! Рядок знайдена. До речі, виявилося що зміщення 450C2868 взагалі не належить нашій програмі (перевірте). Відмінно, колега, от ми і розплутали цю справу.

Ось, мабуть, і закінчено наше цікаве (сподіваюся), оповідання, яке (знову ж таки, сподіваюся) допомогло вам подивитися на програмування в іншому ракурсі і порушило бажання продовжити "низькорівневі вишукування". Що ж, якщо так, то пишіть мені про те, що вас цікавить, і, я сподіваюся, незабаром у нього з'явиться продовження. Будьте здорові!