Сидя на голом железе

Автор: (c)Крис Касперски ака мыщъх

В программировании голого железа есть какое-то непередаваемое словами очарование в котором мало практической пользы, но зато огромное эстетическое удовлетворение и наслаждение. Сегодня мы спустимся на самый низкий уровень, который только возможно достигнуть на IBM PC! Держитесь, мы будем программировать не только без операционной системы, но даже без BIOS'а!

Введение

На прикладном уровне обитать неинтересно. Здесь все приходится делать через готовые интерфейсы (типа Win32 API и иже с ними), громоздящиеся своими иерархическими слоями друг на друга. С этой точки зрения программирование под x86 мало чем отличается от той же Альфы. Мы упираемся в операционную систему, становясь ее пленниками, заключенными в тесную клетку со спертым воздухом.

Без оси жизнь становится намного более захватывающей и интересной. Можно напрямую обращаться ко всем портам ввода-вывода (и знать, что это реальные порты, а не какие-то там виртуальные), выполнять все привилегированные инструкции, переходить в защищенный режим и возвращаться обратно. В общем, делать все, что заблагорассудится. Вот только...

...основное компьютерное оборудование (и, в первую очередь, чипсет) на этом уровне нам неподвластно. Точнее - подвластно, но не совсем. Конфигурирование и настройка чипсета осуществляется на стадии начальной инициализации компьютера, во время загрузки BIOS, подготавливающей его к работе. Некоторые параметры могут быть изменены позднее как через порты ввода/вывода, так и через саму BIOS, но основной фундамент остается непоколебимым. А жаль... ведь далеко не всякая версия BIOS использует возможности аппаратуры на 100%, к тому же лишает нас радости живого общения с железом, подавая его на стол готовеньким. А если нам хочется отведать мяса с кровью?! Тогда необходимо перепрограммировать BIOS, а точнее - небольшую его часть, называемую boot-блоком и наиболее приближенную к аппаратуре.

Перепрограммирование boot-блока открывает огромные возможности для трюкачества, позволяя раскрыть свой творческий потенциал и показать, на что ты способен. Да что там говорить! Это по-настоящему сложно, а значит, реально круто!

Исходные реагенты или что нам понадобится

Прежде всего нам понадобится материнская плата, которую не жалко потерять (в том смысле, что ее смерть не станет трагедией). Еще нужен программатор (поскольку не все матери дают прошивать boot-блок, а за пределами boot-блока жизнь уже становится не такой интересной), который легко купить на радиорынке; любая программа для редактирования BIOS'a (обычно идущая на диске, прилагаемом к материнской плате или лежащая на сайте производителя); прошивка BIOS'а для изучения и подражания (скопированная из самого BIOS или скачанная с сайта); транслятор ассемблера, умеющий генерировать двоичные файлы (FASM, NASM) и, наконец, документация на чипсет (Intel и AMD раздают ее бесплатно, остальные производители - только своим партнерам, зачастую под подписку о неразглашении, как будто там есть, что разглашать).

Ликбез или как все это работает

После "холодной" перезагрузки или включения питания процессор, находящийся в реальном режиме, передает управление по адресу F000h:FFF0h, где находится точка входа в BIOS. В древних IBM AT микросхема постоянной памяти физически "висела" на процессорной шине, непосредственно отображаясь на 64-Кбайтный регион памяти от F000:0000 до F000:FFFF. Современные прошивки в этот объем уже не вмещаются и занимают порядка 512 Кбайт (да и то в упакованном виде), что составляет половину адресного пространства реального режима. Поэтому в память непосредственно отображаются лишь 64 Кбайт прошивки (порядка 4 Кбайт из которых составляет boot-блок), а остальные части прошивка должна уметь считывать из микросхемы Flash-BIOS'а самостоятельно, обращаясь к специальному контроллеру, как правило, "вживленному" в южный мост чипсета и соединенному с BIOS'ом по LPC или ISA-шине.

Flash-BIOS

Листинг 1. Flash-BIOS, подключенный к южному мосту чипсета Intel 915 через шину LPC.

В тот момент, когда BIOS получает управление, практически все имеющееся оборудование к работе еще не готово. Нет даже оперативной памяти, поскольку DRAM-контроллер не настроен и не инициализирован. Короче, как дальше жить? Поэтому первым делом boot-блок проводит первичную инициализацию важнейших узлов, после чего считывает основной код BIOS'а и распаковывает его в оперативную память. На этом работа boot-блока закачивается и всю дальнейшую инициализацию системы выполняет распакованный им код.

Помимо инициализации BIOS также управляет оборудованием (например, выключает жесткие диски по прошествии определенного времени, следит за показанием датчиков напряжения и температуры, регулируя частоту процессора и оперативной памяти), а также предоставляет в распоряжение программиста обширную библиотеку функций, абстрагирующую его от конкретного железа и доступную через вектора прерываний. В частности, за дисковую подсистему отвечает прерывание 13h, но вернемся к нашим баранам, то есть к голому железу.

Обычно, boot-блок располагается в последних 4 Кбайтах файла прошивки, а по смещению 10h от его конца находится точка входа в BIOS, представляющая собой jmp на подлинную точку входа (см. листинги 1, 2). Остальной код прошивки, как правило, упакован, поэтому непосредственно внедряться можно только в последние 4 Кбайта, т.е. в промежуток между F000:EFFF и F000:FFFF.

Внедряемый код должен быть либо полностью перемещаемым (т.е. сохранять свою работоспособность независимо от базового адреса загрузки), либо в начало ассемблерного листинга необходимо воткнуть директиву "ORG 0XXXXh", где 0XXXXh равно разнице конца boot-блока (лежащего по смещению FFFFh) и размеру внедряемого кода.

0007FFF0:  EA 5B E0 00-F0 2A 4D 52-42 2A 02 00-00 00 60 FF

Листинг 2. Последние 10h байт прошивки материнской платы EPOX EP-5EPA+.

0007FFF0: EA5BE000F0       jmp 0F000:0E05B

Листинг 3. ...и их дизассемблерный код.

Последние два байта boot-блока занимает контрольная сумма, рассчитываемая по следующему алгоритму (сохранившемуся еще со времен первых BIOS'ов): мы просто складываем все байты друг с другом и находим остаток от деления на 100h, что в псевдокоде занимается так: sum = (sum + next_byte) & 0xFF. Контрольная сумма всего boot-блока должна равняться нулю, следовательно, последний байт блока равен: (100h - sum) & 0xFF.

Работа с шиной PCI

Шина PCI является основной шиной, через которую к процессору подключаются все остальные контроллеры и устройства, поэтому, чтобы научиться программировать голое железо, нам прежде всего необходимо разобраться, как программировать саму шину PCI. Это легко. Достаточно выучить всего пару регистров: CF8h и CFCh.

Северный мост чипсета

Рисунок 1. Северный мост чипсета, несущий на своем борту важнейшие устройства (такие, например, как контроллер памяти), подключается к процессору через PCI-шину.

В порт CF8h заносится адрес регистра, с которым мы хотим работать (называемый также смещением или offset'ом), а через порт CFCh происходит обмен данными, который в зависимости от конструктивных особенностей конфигурируемого контроллера может быть доступен как на запись/чтение, так и только на чтение. Под "регистром" здесь понимается отнюдь не регистр процессора (типа EAX), а регистр контроллера. Некоторые регистры отображаются на порты ввода/вывода (и тогда с ними можно работать командами IN/OUT), некоторые - нет и с ними можно работать только через CF8h/CFCh.

Регистры аппаратных устройств

Рисунок 2. Регистры аппаратных устройств, отображаемые на адресное пространство и порты ввода/вывода.

Большинство регистров представляют собой совокупность управляющих битов, поэтому перед тем, как что-то записывать в порт CFCh, мы, как правило, сперва должны прочитать текущее состояние чипсета, взвести/опустить нужные нам биты при помощи операций OR и AND, после чего затолкать обновленный регистр на место (однако, если текущее состояние нас не интересует, выполнять операцию чтения совершенно необязательно).

Описание самих регистров можно найти в документации на северный и южный мосты чипсета. Где-то там будет раздел "PCI Configuration Registers", "Registers Description" или что-то в этом роде.

В частности, регистр 114h северного моста чипсета Intel 915, управляет таймингами DDR-памяти и делает он это с помощью своих битов, комбинация которых задает то или иное состояние DRAM-контроллера. Конкретные значения приведены в таблице 1, которую можно найти на 102 странице оригинального руководства: www.intel.com/design/chipsets/datashts/301467.htm:

БитыДоступЗначение по умолчаниюНазначение
31...24--Зарезервированы
23...20R/W9Величина tRAS (она же DRAM Precharge Delay, она же Active to Precharge Delay, она же Precharge Wait State, она же Row Active Delay, она же Row Precharge Delay) устанавливает минимальный промежуток времени между открытием/закрытием одной DRAM-страницы. значения от 0 до 3 зарезервированы, значения от 4 до 15 - время в тактах
19RO0tRAS MAX - максимальный промежуток времени, в течении которого DRAM-страница может оставаться открытой и если контроллер не закроет ее, произойдет Panic Refresh (экстренное обновление), сопровождаемое закрытием всех страниц во всех банках; в данном чипсете этот регистр доступен только на чтение, то есть управление tRAS MAX не реализовано и составляет 120 наносекунд
18...10--Зарезервированы
09...08R/W01bВеличина tCL, более известная под именем CAS# Latency, задает количество тактов между отправкой DDR-микросхеме команды чтения (не записи!) и сбросом первой порции данных на шину, при этом DRAM-страница должна быть заблаговременно открыта, за что отвечает тайминг tRCD.
Значение регистраDDR tCL, тактыDDR2 tCL, такты
00b35
01b2,54
10b23
11Зарезервированы
07--Зарезервирован
06...04R/W010bВеличина tRCD, также называемая RAS# to CAS# Delay или Active to CMD, определяет время открытия DRAM-страницы, в процессе которого со строки конденсаторов считывается заряд и заносится в буфер статической памяти, локально обрабатывающий все последующие обращения.
Значение регистраtRCD, такты
000b2
001b3
010b4
011b5
100bЗарезервированы
101b
110b
111b
3--Зарезервирован
2...0R/W010bВеличина tRP (она же RAS# Precharge Delay, она же Precharge to active) определяет время закрытия DRAM-страницы, в процессе которого происходит возврат данных в банк памяти и его перезарядка. Во время перезаряда банк недоступен, но доступны все остальные банки (большинство DDR-модулей содержит четыре таких банка). Банк закрывается на перезарядку всякий раз, когда происходит обращение к другой странице из этого же самого банка.
Значение регистраtRP, такты
000b2
001b3
010b4
011b5
100bЗарезервированы
101b
110b
111b

Таблица 1. Управление таймигами памяти через регистр 114h северного моста чипсета Intel 915 (на других чипсетах номер регистра и назначение бит с вероятностью, близкой к единице, будут совсем другими, поэтому данная таблица приводится только как пример).

Таким образом, чтобы настроить память на максимальную производительность, необходимо занести в регистр 114h число 10000000000001000000000b (или 400200h в шестнадцатеричном виде), что на языке ассемблера делается так:

        mov eax, 114h      ; регистр чипсета, управляющий DRAM-контроллером
        mov dx, 0CF8h      ; PCI-порт (адрес регистра)
        out dx, eax        ; выбираем регистр

        mov dx, 0CFCh      ; PCI-порт (данные)
        in eax, dx         ; читаем содержимое регистра 114h
        or eax, 400200h    ; конфигурируем таймиги памяти
        out dx, eax        ; записываем регистр чипсета

Листинг 4. Ассемблерный код, устанавливающий таймиги DDR-памяти на максимальную производительность, игнорируя информацию записанную в SPD (только для чипсета Intel 915! обладатели других чипсетов должны заменить константы в соответствии со своей документацией).

Заключение

Общаться с оборудованием на "железном" уровне безумно интересно, но чрезвычайно утомительно и сложно. Прежде, чем контроллер оперативной памяти "заведется", предстоит проделать немало работы и тщательно прочитать порядка тысячи страниц документации, поскольку любая пропущенная мелочь может пустить все кувырком. Положение усугубляется полным отсутствием отладочных средств, что дисциплинирует и учит искать ошибки "глазами".

Совет: прежде чем писать свои мини-BIOS, выводящий на экран зеленый травянистый семилистник под мелодию "семь сорок", раздающуюся из спикера, скачайте готовую прошивку и дизассемблируйте boot-блок, разобравшись, как он работает. Мы увидим много обращений к портам, часть из которых удается расшифровать с помощью документации на чипсет, часть - обратившись к описанию остальных контроллеров и микросхем, установленных на плате.

seg010:DB6D 55                 push     bp
seg010:DB6E 8B EC              mov      bp, sp
seg010:DB70 83 EC 08           sub      sp, 8
seg010:DB73 C6 45 F8 41        mov      byte ptr [di-8], 'A'
seg010:DB77 C6 45 F9 4D        mov      byte ptr [di-7], 'M'
seg010:DB7B C6 45 FA 49        mov      byte ptr [di-6], 'I'
seg010:DB7F C6 45 FB 42        mov      byte ptr [di-5], 'B'
seg010:DB83 C6 45 FC 49        mov      byte ptr [di-4], 'I'
seg010:DB87 C6 45 FD 4F        mov      byte ptr [di-3], 'O'
seg010:DB8B C6 45 FE 53        mov      byte ptr [di-2], 'S'
seg010:DB8F C6 45 FF 43        mov      byte ptr [di-1], 'C'
seg010:DB93 53                 push     bx
seg010:DB94 56                 push     si
seg010:DB95 33 F6              xor      si, si
seg010:DB97 8B 45 08           mov      ax, [di+8]
seg010:DB9A 8B 50 20           mov      dx, [bx+si+20h]
seg010:DB9D 85 D2              test     dx, dx
seg010:DB9F 74 3A              jz       short loc_A9BDB

Листинг 5. Фрагмент прошивки AMI-BIOS.

В конечном счете, мы создадим свой boot-блок, ассемблируем и, воткнув в программатор FLASH-BIOS, зальем туда свое творение (предварительно сохранив оригинальную прошивку). Не стоит надеяться, что материнская плата "проглотит" его с первого раза. Будет только черный экран, не реагирующий на клавиатуру и все. Даже курсора не будет. Попробуй угадай, в каком месте сидит ошибка!!! Но ведь только так - из юноши программисты становятся настоящими мужчинами. В смысле - хакерами. После недели-другой непрекращающихся мытарств мы, наверное, сможем оценить - как приходилось нашим предкам, дырявящим перфокарты и совсем незнакомым с понятием интерактивной отладки.

Порядок инициализации оборудования

Порядок инициализации оборудования не высечен на камне и допускает довольно большие вольности, однако определенные традиции и нормы приличия все-таки нужно соблюдать.

Материнские платы от EPOX выгодно отличаются тем, что имеют 2-разрядный сегментный индикатор, отображающий ход загрузки, что облегчает диагностику поломки (если таковая есть) и упрощает дизассемблирование BIOS, поскольку... код, отображаемый на индикаторе, в листинге присутствует в виде константы, расшифровку значения которой можно найти в руководстве в приложении E. Лучших комментарий к листингу, пожалуй, и не придумаешь!!!

На остальных материнских платах порядок инициализации оборудования не сильно отличается от EPOX'а и все происходит приблизительно следующим образом:

Ссылки по теме