#16 Путешествие по памяти

Вступление

Здравствуйте!
Сегодня мы продолжим изучать память и даже отправимся в короткое путешествие по ней. Ещё раз напоминаю, что рассылка переехала на сайт
http://www.web-pascal.narod.ru и теперь всю последнюю информацию можно получить там. Также наконец-то был благополучно пополнен архив рассылки, который теперь можно скачать с сайта (http://www.web-pascal.narod.ru/ras/archiv.rar).

Новости сайта http://www.web-pascal.narod.ru

Теория

В прошлый раз я рассказывал об устройстве памяти. Сегодня мы научимся использовать эту самую память. В паскале существует специальный тип - указатели. Переменная указатель - это ссылка на данные или код. Указатель это вам не это :) Значение переменной указателя - это адрес памяти. А что распологается по этому адресу решать вам.

Но сначала несколько общих слов и соображений. Во первых некоторые сокращения: RAM (Random Access Memory) - память общего доступа, т.е. память с которой мы можем делать всё, что захотим. ROM (Read Only Memory) - память только для чтения, в неё мы не можем ничего записать. Ещё одно соображение: мы пишем программы для операционной системы DOS. В Windows большинство DOS программ работает нормально. Однако в WinNT, 2k, XP на 99% уверен, что возникнут проблеммы с работой программ, приведённых ниже. Всё дело в том, что Windows не "любит" когда кто-то работает с памятью, а ему об этом не слова. Поэтому он ограничивает работу таких программ, давая им уверенноть, что они по работали с памятью, а на самом деле ничего такого не произошло. Так что если что-то не сработает, не надо сразу завалить меня гневными письмами. Единственный выход из такого положения: создать загрузочную дискетку с DOS'ом и загрузившись с неё запускать программы.

Для получения адреса переменной вам нужно указать перед её именем знак "собака" - @. Т.е. если

i : integer;
то @i - это адрес переменной i.

Для объявления указателя нам надо создать переменную соответствующего типа. Для начала мы рассмотрим самый простой тип указателей - безтиповые указатели :) Они могут указывать (т.е. хранить адрес) чего угодно. Хотите адрес переменной - пожалуйста, адрес записи - с легкостью, процедуры - ни каких проблем. Имя у этого типа - Pointer (англ. указатель).

Напишем такую программку:

var
 i : integer;
 p : pointer;

begin
  i := 3;
  p := @i
end.
а теперь вопрос, что содержится в p (после выполнения программы) ? Ответ адрес переменной i. Если вы посмотрите на указатель p в отладчике, то вы увидите, что он равен Ptr($13C1,$50) (надо довести выполнение программы до полного завершения). Что это такое? Для работы с адресами в паскале есть функция:
function Ptr(Seg, Ofs: Word): Pointer;
она преобразовывает адрес сегмента (Seg) и смещения (Ofs) к указателю. Знак доллара - "$" - перед числом означает, что оно шеснадцатеричное. это специфическое обозначение паскаля. Я при объяснении буду использовать букву h, а в программах мы увидим знак $. Просто это одно и тоже 10h = $10 = 16. Если вы слабо владеете этой системой - обязательно прочитайте выпуск #2 Системы счисления ч1 Шестнадцатеричная

Так вот Ptr($13C1,$50) означает, что наш указатель указывает на область памяти с сегментом 13С1h и смещением в этом сегменте 50h. Поменяем местами объявления p и i.

var
 p : pointer;
 i : integer;

begin
  i := 3;
  p := @i
end.
Что же тогда произойдёт? Казалось бы ничего такого - просто поменяли местами объявления двух переменных, что от этого может изменится ? Оказывается может! Давайте посмотрим на это дело в отладчике... действительно p теперь равен Ptr($13C1,$54). Почему такое произошло ? Мы перераспределяем память в сегменте данных. Т.е. в первом случае у нас сначала в памяти отводится место под переменную i, а потом идёт указатель. Во втором случае память сначала выделяется под указатель, а только потом под i. Поэтому смещение в обоих случаях разное.

Указатель без типа хорошо, ну а с типом лучше! Для использования таких указателей лучше создать новый тип. Перед именем того типа, на который наш указаетль указывает нужно поставить символ каре - "^".

type
pinteger = ^integer;
Теперь мы можем создать указатель на переменную типа integer. И присвоить указателю её адрес.
var
 i : integer;
 p : pinteger;

begin
  i := 3;
  p := @i;
Однако наверняка вам хочется знать, что же расположено по адресу на который указывает p ?? Для этого существует приём, который называется "разыменование указателей". Суть его в том, что разыменовав указатель вы можете работать с ним так, как если бы это была переменная того типа, на который он указывает. Чтобы разыменовать указатель, поместите ^ после его имени.
begin
  i := 3;
  p := @i;
  write (p^);
выдаст на экран значение 3. Используя символ ^ мы указываем, что надо использовать указатель не как адрес, а как то что содержится по этому адресу. На что указывает p ? На адрес переменной i. Т.е. изменив i мы изменим значение по адресу p. А изменив p, мы получим в p указатель на неизвестно что.

При работе программы используются, не имена переменных, а их адреса. Так как в р у нас адрес i, то изменив значение по этому адресу (по адресу в р) мы изменим значение i. А изменив значение i, мы изменим только значение 2-х байтов по адресу р (и соответственно изменим p^).

Например чему будет равно i после такого:

begin
  i := 3;
  p := @i;
  p^ := 123;
Ответ i = 123. Как? Ведь мы i нигде не изменяем ??

Что делает эта строчка p^ := 123; - она помещает по адресу p значение 123. А адрес р равен адресу i.
Для нашего случая условно говоря p^ = i !!!

Однако поменяв адрес p на что-то ещё p^ уже будет не равно i !!!

Важно вбить себе в голову, что указатель (р) - это только адрес, по которому может распологаться всё, что угодно. А p^ - это значение по этому адресу.

На один и тот же адрес может указывать бесконечное число указателей. Например такой код:

var
 i : integer;
 p1, p2 : pinteger;

begin
  i  := 3;
  p1 := @i;
  p2 := p1;
  p2^ := 123;
  writeLn (i)
end.
Опять тот же вопрос: чему равно i ? Ответ i = 123. Давайте разберёмся построчно и с отладчиком. Кстати в отладчике вместо адреса сегмента данных указывается Dseg (от англ DateSegment - сегмент данных). Это результат такой функции function DSeg: Word; Она возвращает адрес сегмента данных.
Итак:
  • i := 3; - смотрим в отладчике: i = 3; p1, p2 = nil. Что это за nil чуть позже и ниже :).
  • p1 := @i; - присваиваем р1 адрес i. ( i = 3; p1 = Ptr(DSeg,$52); p2 = nil)
  • p2 := p1; - присваиваем р2 значение р1, т.е. присваиваем р2 адрес i (i = 3; p1, p2 = Ptr(DSeg,$52))
  • p2^ := 123; - помещаем по адресу p2 значение 123. А что у нас за адрес в р2 ? Адрес i. Значит мы изменяем значение по этому адресу, т.е. значение i (i = 123; p1, p2 = Ptr(DSeg,$52))
Вернёмся к nil. Когда указатель равен nil это означает, что он указывает ни на что. Т.е. не проинициализированный указатель.

Программа

Сегодня мы не будем писать что-то особенное, а просто полазим по некоторым адресам в памяти. Давайте вспомним распределение памяти:

  • 00000h - 9FFFFh - Base Memory, 640 Кбайт - стандартная память, доступная программам реального режима.
  • A0000h - FFFFFh - Upper Memory Area (UMA), 384 Кбайта - верхняя память, зарезервированая для системных нужд. В ней размещается память адаптеров.
  • 10000h и выше - Extended Memory - дополнительная память, непосредственно доступная только в защищенном режиме процесора.
Extended memory нас не интересует, т.к. для нас она не доступна. Обратим наш взгляд на UMA - столько всего интересного... Давайте для начала полазаем по памяти нашего видеоадаптера. В зависимости от его типа она занимает следующие адреса:
  • MDA RAM - B000h - B0FFFh
  • CGA RAM - B800h - BBFFFh
  • EGA ROM - C000h - C3FFFh
  • VGA ROM - C000h - C7FFFh
  • EGA, VGA RAM - A000h - BFFFFh, в зависимости от режима используют следующие области:
    • Графика - A000h - AFFFFh
    • Цветной текст - B800h - BFFFFh
    • Моно текст - B000h - B7FFFh
Думаю, что у всех сейчас стоит VGA (SVGA тоже сюда относится). Так что разговор пойдёт о нём. Наши программы работают в текстовом режиме (существует ещё и графический, но о нём позже). К тому же в цветном, поэтому видеопамять распологается по этим адресам B800h - BFFFFh . В этом режиме под символ отводится 2 байта (или одно слово: сам символ + аттрибуты (цвет)). Например давайте прочитаем самый первый символ на экране. Мы можем читать только 1 байт, т.к. сначала идёт байт, который отвечает за символ, а потом байт-аттрибут.
type
  pbyte = ^byte;

var
 video : pbyte;

begin
  video := Ptr ($B800, 0);
  writeLn (chr(video^))
end.
Если вы не очищали экран и запускали программу из среды BP, то должна вывестись буква B (первый символ на экране).
А теперь вообще чудный фокус: давайте изменим значение этого первого символа на весёлую рожу. Изменим строки:
begin
 video := Ptr ($B800, 0);
 video^ := 1
end.
Запустим... J ... порадуемся. Сейчас мы использовали метод, называемый прямой доступ к памяти (в нашем случае к видео памяти). Это самый быстрый способ вывода символа на экран.
Так как под символ отводится слово, то мы можем в байте-аттрибуте задать цвет символа, цвет фона и ещё кое что. Биты байта-аттрибута отвечают за следующее:
Фон Текст
7 6 5 4 3 2 1 0
BL R G B I R G B
Буквы RGB представляют собой комбинацию для цвета. R - красная составляющая, B - синяя, G - зелёная. Бит 7 (BL) - отвечает за мигание символа, а бит 3 (I) - интенсивность свечения. Т.е. если мы хотим вывести мигающий символ красным цветом на чёрном фоне, то мы должны составить такую комбинацию 10000100. Давайте изменим код предыдущей программы таким образом:
begin
 video := Ptr ($B800, 1);
 video^ := $84
end.
Мы изменили смещение на 1 байт, поэтому теперь video^ это не символ, а байт-аттрибут первого символа на экране. Изменив значение по этому адресу мы изменим значение байта-аттрибута первого символа. Если там оставалась весёлая рожица, то она станет красной и начнёт моргать, если там оставалась буква В, то тогда это приключится с ней.

Однако помимо экрана (кстати запомните этот адрес B800h - он может встречаться довольно часто), так вот есть ещё куча интересных мест, куда нам следует сунуть свой нос. Например слово по адресу 0:413 - в нем содержится информация BIOS'a о наличной памяти в килобайтах. Оно заполняется во время теста POST. Что бы узнать значение по этому адресу напишем такую "програмку".

type
 pword = ^word;

var
 p : pword;

begin
  p := Ptr (0, $413);
  writeLn (p^)
end.
ничего сложного. Однако, если нам понадобится не байт, не два, а сразу много ? Тогда надо создать указатель на массив. Например 8 байт по адресу FFFF:5 - это дата изготовления BIOS'a.
type
 tarray = array [0..7] of byte;
 p8byte = ^tarray;

var
 p : p8byte;
 i : integer;

begin
  p := Ptr ($FFFF, 5);
  for i := 0 to 7 do
    write (chr(p^[i]))
end.
При загрузке компьютера в момент, когда определяется количество памяти (тест POST), нажмите на клавишу Pause - и посмотрите в левый нижний угол экрана - там обычно эта дата и печатается. Однако кроме того, что мы указали на массив из 8 байт, мы могли указать на 1 байт, и в цикле менять смещение. Память вообщем отдалённо напоминает простой массив.

Можно посвятить целую рассылку, перечисляя адреса памяти, по которым можно полазить и узнать что-нибудь полезное, однако на сегодня хватит. К тому же подобную информацию легко найти.


[Назад] [Содержание] [Дальше]
Hosted by uCoz