Автор: (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-шине.
Листинг 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. Это легко. Достаточно выучить всего пару регистров: 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...20 | R/W | 9 | Величина tRAS (она же DRAM Precharge Delay, она же Active to Precharge Delay, она же Precharge Wait State, она же Row Active Delay, она же Row Precharge Delay) устанавливает минимальный промежуток времени между открытием/закрытием одной DRAM-страницы. значения от 0 до 3 зарезервированы, значения от 4 до 15 - время в тактах | ||
19 | RO | 0 | tRAS MAX - максимальный промежуток времени, в течении которого DRAM-страница может оставаться открытой и если контроллер не закроет ее, произойдет Panic Refresh (экстренное обновление), сопровождаемое закрытием всех страниц во всех банках; в данном чипсете этот регистр доступен только на чтение, то есть управление tRAS MAX не реализовано и составляет 120 наносекунд | ||
18...10 | - | - | Зарезервированы | ||
09...08 | R/W | 01b | Величина tCL, более известная под именем CAS# Latency, задает количество тактов между отправкой DDR-микросхеме команды чтения (не записи!) и сбросом первой порции данных на шину, при этом DRAM-страница должна быть заблаговременно открыта, за что отвечает тайминг tRCD. | ||
Значение регистра | DDR tCL, такты | DDR2 tCL, такты | |||
00b | 3 | 5 | |||
01b | 2,5 | 4 | |||
10b | 2 | 3 | |||
11 | Зарезервированы | ||||
07 | - | - | Зарезервирован | ||
06...04 | R/W | 010b | Величина tRCD, также называемая RAS# to CAS# Delay или Active to CMD, определяет время открытия DRAM-страницы, в процессе которого со строки конденсаторов считывается заряд и заносится в буфер статической памяти, локально обрабатывающий все последующие обращения. | ||
Значение регистра | tRCD, такты | ||||
000b | 2 | ||||
001b | 3 | ||||
010b | 4 | ||||
011b | 5 | ||||
100b | Зарезервированы | ||||
101b | |||||
110b | |||||
111b | |||||
3 | - | - | Зарезервирован | ||
2...0 | R/W | 010b | Величина tRP (она же RAS# Precharge Delay, она же Precharge to active) определяет время закрытия DRAM-страницы, в процессе которого происходит возврат данных в банк памяти и его перезарядка. Во время перезаряда банк недоступен, но доступны все остальные банки (большинство DDR-модулей содержит четыре таких банка). Банк закрывается на перезарядку всякий раз, когда происходит обращение к другой странице из этого же самого банка. | ||
Значение регистра | tRP, такты | ||||
000b | 2 | ||||
001b | 3 | ||||
010b | 4 | ||||
011b | 5 | ||||
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'а и все происходит приблизительно следующим образом: