Предыдущие части:
1. Общие сведения. Legacy-таймеры PIT и RTC.
2. Шина PCI – таймер менеджера питания ACPI.
3. ACPI таблицы – таймер HPET.
4. Таймер контролёра Local-APIC.
5. Счётчик процессора TSC.
6. Win - профилирование кода.
---------------------------------------
Основным недостатком рассмотренных ранее таймеров является время доступа к ним, т.к. все эти устройства расположены за пределами центрального процессора CPU. Чтобы дотянуться до их контроллёров, процессор вынужден ждать освобождения шины FSB/DMI, посредством которой он тесно связан с чипсетом. Это тормозит процесс и несколько снижает ценность таймеров. Но есть и другая проблема..
Поскольку шина в системе одна, то с приходом многоядерных CPU тучи сгустились. Движимые инстинктом выживания, все ядра хором набрасывались на общую шину и под натиском грубой силы она проседала – запросы выстраивались в длинную очередь и шина фактически становилась узким местом в архитектуре. Здесь стало очевидно, что гендерные нормы уже устарели и не могут использоваться в наши дни. Так появился снискавший себе заслуженную славу локальный (по отношению к процессору) контролёр прерываний "Local APIC" – рассмотрим его работу подробней..
APIC – общие сведения
Если процессор не вещь-в-себе, он должен выстраивать систему социальных взаимоотношений с устройствами. Исторически, для этих целей были предусмотрены линии IRQ – Interrupt Request – запрос на прерывание. Любое (способное вести диалог) устройство обязано иметь эту линию в своей конструкции: например клавиатура посылает по ней сигнал IRQ при каждом нажатии на клавишу, диск – по готовности данных, лан – по приходу пакета, таймер – по истечении времени, и т.д. Таким образом получаем N-ное количество сигнальных линий, число которых равно числу активных устройств на материнской плате.
Теперь эта шина-IRQ заходит в системный контролёр прерываний PIC – Peripheral Interrupt Controller – древний девайс эпохи неолита. Он мог обслуживать всего 8 устройств, т.к. имел именно 8 входных линий, и 1 выход INTR к процессору. Но на плате источников прерываний IRQ могло быть больше, поэтому в архитектуре было два PIC’a, которые соединялись каскадно – выход INTR первого заходил на вход второго, итого получали уже 15-входов. На момент появления этих контролёров, ни о каких мульти/процессорных системах SМР никто даже не слышал, так-что PIC изначально был заточен на взаимодействие с одним процессором и со-временем от него пришлось избавиться.
Основным преимуществом усовершенствованного
Рассмотрим основные поля представленной таблицы..
Первый столбец "INT-IN" – это входные пины в IOAPIС. Во-втором столбце лежит "вектор прерывания" – это порядковый номер дескриптора в системной таблице IDT (именно в дескрипторах хранятся указатели на процедуры обработки прерываний). Мы можем маскировать векторы, в результате чего процессор не будет реагировать на них. Последний "Destination" определяет получателя, которыми могут быть как процессор, так и логические его ядра, адресуемые по их LAPIC-ID. Третий столбец "Delivery Mode" – это режим доставки прерываний из IOAPIC в LAPIC. Всего имеются 6-вариантов, для которых выделяются 3-бита:
Вся эта информация хранится в регистрах IOAPIC.REDIRECTION.TABLE контролёра. Сам контролёр отображается на системную память по адресу
Здесь видно, что в нижней таблице первые три регистра с индексами 0..2 являются информационными и доступны только для чтения Read-Only. Зато дальше, (начиная с индекса 0х10 и вплоть до 0x1ЕE) выстроились в ряд упомянутые выше 64-битные регистры IOAPIC.REDIRECTION.TABLE. Под каждую из 240-линий IRQ выделяется индивидуальный регистр, где и хранятся исчерпывающие свойства данного прерывания (см.назначение бит в таблице).
Вот демонстрационный пример по сбору данных, которые показал нам софт "PCIScope" на втором рисунке. Здесь, начиная с индекса
Обратите внимание, что в данном случае векторы всех прерываний замаскированы логической единицей. Значит при включении машины как биос так и новороченый EFI используют устаревший контролёр PIC с его 15- входами. Снимает маску и включает IOAPIC позже уже загрузчик операционной системы, когда переходит в защищённый режим и выстраивает свою таблицу IDT – Interrupt Descriptor Table. Кроме того на х64 видно, что контролёр имеет 120 входных пинов IRQ, в то время-как на х32 их всего 24.
Отметим, что прерывания IRQ посылают только физические устройства, поэтому они относятся к классу "аппаратных". Есть ещё и программные прерывания, например
Шина APIC и векторы прерываний
В масштабах истории, моногамный брак APIC+LAPIC появился относительно недавно. Во-времена господства древнего контролёра PIC, взаимосвязь его с CPU осуществлялась по системной пар-шине FSB. В современной-же архитектуре, IOAPIC общается с локальным контролёром посредством специально выделенной для этих целей трёхпроводной последовательной шины 3-Ware-APIC (см.рис.1). Этот тоннель позволяет освободить системную шину DMI от проблем обслуживания прерываний.
По двум из трёх сигнальных линий 3-Ware-APIC гуляет 64-битный трафик в виде данных регистра IOAPIC.RED.TABLE, а третья линия – сигнал тактовой частоты. Протокол новой шины поддерживает механизм арбитража ядер по приоритетам 0-15, а сам приоритет динамически меняется после каждой передачи сообщений.
Получив 64-битные данные, LAPIC выделяет из них вектор прерывания, который зашит в младшем байте пакета (см.последнюю таблицу). Теперь логика умножает этот вектор на 8 (размер дескриптора в таблице IDT) и таким образом получает адрес обработчика возникшего прерывания для последующего его вывоза.
В системной таблице IDT собраны 8-байтные дескрипторы прерываний - всего их 256. На эту таблицу указывает 6-байтный регистр
На этом рисунке, внутри зелёного блока видим дескрипторы прерываний, каждый из которых имеет свой вектор (порядковый номер в данной таблице). Например по вектору(0) лежит дескриптор со-значением
Процессоры х86 делят дескрипторы на три типа: для шлюзов прерываний Int, ловушек Trap и задач Task.
Мы остановимся лишь на шлюзе-прерываний, который имеет тип
Так из дескриптора мы вытащили указатель на функцию прерывания, и командой "Unassembler" можно запросить код его обработчика:
WinDbg имеет спец/расширение для просмотра таблицы IDT – запрашивается оно командой !idt (просмотр списка только активных векторов), или она-же с параметром -a (вывод полного списка 256, включая зарезервированные исключения с векторами 0..1Fh):
В процессе формирования таблицы IDT нужно придерживаться определённых правил..
Во-первых, таблица должна быть заполнена полностью, т.е. состоять из всех 256 дескрипторов. Если вектор не используется, отсылаем его к заглушке
Во-вторых, первые 32-вектора процессор рассчитывает получить в своё распоряжение и мы не должны препятствовать ему в этом. Например при ошибке деления на нуль
Таймер Local-APIC
Частота = системная шина | счётчик = 32 бит
Реакция локального APIC на обработку аппаратных прерываний достаточно прозрачна. Если в двух словах, то внешний IOAPIC получает IRQ от устройства Х, и по цепочке пересылает в LAPIC связанный с ним 64-битный свой регистр IOAPIC.RED.TABLE. Дальше LAPIC извлекает из этих 64-бит вектор-прерывания и ставит его в очередь "DPC-Queue" до лучших времён. Позже, векторы вываливаются из очереди и по ним управление передаётся в системную таблицу IDT, от куда и происходит вызов соответствующего обработчика.
Однако ядро процессора имеет и свои/внутренние устройства контроля – всего их 6, в числе которых и некая пародия на таймер. Для обслуживания этих источников, в LAPIC предусмотрена таблица LVT – Local Vector Table. Это закрытая для всех зона и существует она как самостоятельный механизм. LVT состоит из шести 32-битных регистров, битовая маска которых представлена в таблице ниже:
Априори, все
Кроме регистра LVT, локальный таймер имеет ещё три регистра, в которых определяются уже его свойства. Он тактируется частотой системной шины FSB/DMI, причём не эффективной (с применением множителя), а реальной частотой от клокера. Как-правило эта частота имеет начальный порог 100/133 MHz и дальше по возрастающей, в зависимости от архитектуры и модели чипсета. Чтобы угомонить разбушевавшийся таймер, можно применить к этой частоте локальный делитель, значение которого указывается в регистре Base+0x3E0 (см.таблицу ниже). Делитель может принимать значение степени двойки до 128, т.е. 1,2,4,8,16,32,64 и 128.
Теперь в регистре Base+0x380 остаётся указать начальное значение счётчика, от которого со-скорость частоты шины он будет стремиться к нулю. Если битом[17] таймер установлен на периодичность (а именно в таком режиме работают таймеры всех протестированных мною систем), то при каждом достижении счётчиком нуля, таймер будет генерить прерывание, а процессор будет передавать управление по указанному нами вектору, в таблицу IDT.
Интересным моментом тут является то, что если инициализировать счётчик макс.значением
Программирование таймера LAPIC представляет собой проблему как в защищённом Win, так и в реальном досе – этому есть несколько причин. Ну с виндой всё понятно.. она не даёт нам доступа к памяти выше
Дело в том, что для своей работы LAPIC требует защищённый режим с таблицей дескрипторов прерываний IDT. А раз так, значит для простой задачи нужно будет писать огромное кол-во обработчиков, чтобы была возможность элементарно выводить инфу на экран, пользоваться клавиатурой, обрабатывать исключения и ещё куча всего. При желании конечно-же можно перекроить под линейные адреса готовые обработчики реального режима, но для обычного демо/примера такая овчинка никак не стоит выделки, поэтому мы поступим иначе..
Если мастдай не хочет пускать нас в своё кернел-пространство, то придётся вытаскивать его контрабандным путём, тупо сбросив 1К-байтный дамп памяти
Чтобы проблема чтения регистров не превратилась в проблему доступа к памяти, мы выберем самый простой путь и воспользуемся сторонней утилитой
После нажатия пимпы ОК мы получим бинарник с именем MFEE00000.bin – это готовый слепок памяти. Вариант удобен тем, что позволяет снять дамп с разных машин и сравнить значения их регистров LAPIC. Напомню, что база (стартовый адрес) у всех будет одинаковой. Теперь, для подключения внешних файлов, ассемблер FASM имеет директиву file, аргументом которой является только имя файла. В своей демке, я выделил под этот бинарник отдельную секцию, чтобы он не путался среди остальных данных. Вот пример:
Этой прожке я вскармливал дампы двух/своих машин – один сделал на семёрке, другой на хрюше. Обратите внимание, что в обоих случаях, системы вообще не используют таймер LAPIC, т.к. векторы всех локальных прерываний в таблице LVT замаскированы – в активном состоянии находится только монитор производительности PerfMon. Однако таймер продолжает работать в холостую, а значит ОС всё-же обращается иногда к нему.
Делитель опорной частоты выставлен в единицу (см.жёлтый блок), зато значение инициализации счётчика разное. Запустив любой софт по сбору информации о железе можно обнаружить, что Init совпадает с частотой системной шины Bus-Speed. Тогда по логике вещей, произведение делителя и значения инициализации должно возвращать рабочую частоту таймера, хотя при других делителях эту теорию проверить мне так и не удалось.
Судя по регистрам LVT, к таймеру привязан вектор
Заключение
В эпилоге темы хотелось-бы отметить, что у каждого ядра процессора своя память и свой контролёр LAPIC. Соответственно базовый адрес их регистров
Другими словами, если прочитать счётчик таймера одного ядра и сравнить его с счётчиком соседнего ядра, то с вероятность 99% значения будут разными, хотя адрес регистров совпадает. Это-же относится и к выключенному состоянию таймера – на ядре(0) он может быть замаскирован, а на ядре(1) включён.
1. Общие сведения. Legacy-таймеры PIT и RTC.
2. Шина PCI – таймер менеджера питания ACPI.
3. ACPI таблицы – таймер HPET.
4. Таймер контролёра Local-APIC.
5. Счётчик процессора TSC.
6. Win - профилирование кода.
---------------------------------------
Основным недостатком рассмотренных ранее таймеров является время доступа к ним, т.к. все эти устройства расположены за пределами центрального процессора CPU. Чтобы дотянуться до их контроллёров, процессор вынужден ждать освобождения шины FSB/DMI, посредством которой он тесно связан с чипсетом. Это тормозит процесс и несколько снижает ценность таймеров. Но есть и другая проблема..
Поскольку шина в системе одна, то с приходом многоядерных CPU тучи сгустились. Движимые инстинктом выживания, все ядра хором набрасывались на общую шину и под натиском грубой силы она проседала – запросы выстраивались в длинную очередь и шина фактически становилась узким местом в архитектуре. Здесь стало очевидно, что гендерные нормы уже устарели и не могут использоваться в наши дни. Так появился снискавший себе заслуженную славу локальный (по отношению к процессору) контролёр прерываний "Local APIC" – рассмотрим его работу подробней..
APIC – общие сведения
Если процессор не вещь-в-себе, он должен выстраивать систему социальных взаимоотношений с устройствами. Исторически, для этих целей были предусмотрены линии IRQ – Interrupt Request – запрос на прерывание. Любое (способное вести диалог) устройство обязано иметь эту линию в своей конструкции: например клавиатура посылает по ней сигнал IRQ при каждом нажатии на клавишу, диск – по готовности данных, лан – по приходу пакета, таймер – по истечении времени, и т.д. Таким образом получаем N-ное количество сигнальных линий, число которых равно числу активных устройств на материнской плате.
Теперь эта шина-IRQ заходит в системный контролёр прерываний PIC – Peripheral Interrupt Controller – древний девайс эпохи неолита. Он мог обслуживать всего 8 устройств, т.к. имел именно 8 входных линий, и 1 выход INTR к процессору. Но на плате источников прерываний IRQ могло быть больше, поэтому в архитектуре было два PIC’a, которые соединялись каскадно – выход INTR первого заходил на вход второго, итого получали уже 15-входов. На момент появления этих контролёров, ни о каких мульти/процессорных системах SМР никто даже не слышал, так-что PIC изначально был заточен на взаимодействие с одним процессором и со-временем от него пришлось избавиться.
Основным преимуществом усовершенствованного
Ссылка скрыта от гостей
стала возможность посылать по выходной линии INTR запросы конкретным ядрам одного процессора, т.е. получили маршрутизацию прерываний. Но для этого потребовалось наличия ещё одного устройства Local-APIC, которым снабдили каждое ядро. Так в типичной конфигурации МР-систем два этих контролёра работают только в паре, и не могут в полной мере выразить себя друг-без-друга. В исключительных ситуациях (когда у процессора нет своего LAPIC) внешний IOAPIC работает в режиме эмуляции устаревшего PIC, ..в остальных-же случаях режим назвали "симметричным", как на рисунке ниже:• Во-первых, у внешнего контролёра IOAPIC общее число входов IRQ увеличилось с 15 до 240, хотя на практике используются гораздо меньше (кол-во активных на данный момент линий можно прочитать из регистра-версии IOAPIC, см.код ниже).
• Во-вторых, локальные контролёры LAPIC способны сами решать проблемы своего ядра посредством 5-ти внутренних прерываний, в числе которых: обработка ошибок, контроль температуры, монитор производительности, и прерывания от работающего на частоте системной шины – локального таймера. Для обслуживания этих прерываний LAPIC имеет собственную таблицу LVT – Local Vector Table. Ещё одним плюсом стал отложенный вызов процедур обработчиков-прерываний DPC -
Ссылка скрыта от гостей
.
• В третьих, под свойства каждого из 240-входов IOAPIC, выделяется индивидуальный 64-битный регистр под названием “IOAPIC.REDIRECTION.TABLE”. В старших его битах [63:54] хранится адрес получателя данного прерывания, которыми могут быть или физический процессор, или-же одно из 256 его ядер с идентификаторами LAPIC-ID. Вот как отображает эту инфу софт “PCIScope”:
Рассмотрим основные поля представленной таблицы..
Первый столбец "INT-IN" – это входные пины в IOAPIС. Во-втором столбце лежит "вектор прерывания" – это порядковый номер дескриптора в системной таблице IDT (именно в дескрипторах хранятся указатели на процедуры обработки прерываний). Мы можем маскировать векторы, в результате чего процессор не будет реагировать на них. Последний "Destination" определяет получателя, которыми могут быть как процессор, так и логические его ядра, адресуемые по их LAPIC-ID. Третий столбец "Delivery Mode" – это режим доставки прерываний из IOAPIC в LAPIC. Всего имеются 6-вариантов, для которых выделяются 3-бита:
Вся эта информация хранится в регистрах IOAPIC.REDIRECTION.TABLE контролёра. Сам контролёр отображается на системную память по адресу
0xFEC00000
, где и находятся 4 привязанных к нему порта. Первый порт называется "INDEX" – через него осуществляется доступ ко всем\остальным регистрам. Второй порт "DATA" позволяет считывать значение регистра по указанному индексу. Например записав нуль в порт-индекса 0xFEC00000
, мы сможем прочитать из порта-данных 0xFEC00010
идентификатор контролёра IOAPIC и т.д.Здесь видно, что в нижней таблице первые три регистра с индексами 0..2 являются информационными и доступны только для чтения Read-Only. Зато дальше, (начиная с индекса 0х10 и вплоть до 0x1ЕE) выстроились в ряд упомянутые выше 64-битные регистры IOAPIC.REDIRECTION.TABLE. Под каждую из 240-линий IRQ выделяется индивидуальный регистр, где и хранятся исчерпывающие свойства данного прерывания (см.назначение бит в таблице).
Вот демонстрационный пример по сбору данных, которые показал нам софт "PCIScope" на втором рисунке. Здесь, начиная с индекса
0х10
и шагом 2 я обхожу все регистры-перенаправлений, попутно сбрасывая на консоль основные их поля и биты. Поскольку Win не даёт нам доступ к кернел-пространству с адресом 0xFEC00000
, то прожка для реального режима с переходом в нереальный:
C-подобный:
org 100h
jmp start
caption db 13,10,' IO-APIC info v.01 '
db 13,10,' ==============================='
db 13,10,' Set 4Gb unreal-mode.......: OK!'
db 13,10
db 13,10,' Read IOAPIC registers'
db 13,10,' IOAPIC-ID...............: $'
ioVersion db 13,10,' IOAPIC Version..........: $'
ioTotalPin db 13,10,' IOAPIC Total IRQ pin....: 0..$'
apicRedTbl db 13,10
db 13,10,' Interrupt Redirection'
db 13,10,' -----+--------+------+----------+----------'
db 13,10,' IRQ | Vector | Mask | Mode | Dest'
db 13,10,' -----+--------+------+----------+---------- $'
intIrq db 13,10,' 0x$'
intVect db ' 0x$'
intMask db ' $'
intMode db ' $'
intDest db ' $'
modeTable dw 0,fixed, 1,lowest, 2,smi, 4,nmi, 5,init, 7,ext
fixed db 'Fixed $'
lowest db 'Lowest $'
smi db 'SMI $'
nmi db 'NMI $'
init db 'INIT $'
ext db 'ExtINT $'
unknown db 'Unknown$'
phy db 'Processor $'
logical db 'LAPIC ID $'
irq db -1
apicIndex dd 0xfec00000 ;// порт-индекса IOAPIC
apicData dd 0xfec00010 ;// порт-данных IOAPIC
counter db 16 ;// счётчик регистров IOAPIC.REDIRECTION.TABLE (у меня всего 16)
index db 0x10 ;// индекс первого IOAPIC.RED.TABLE
;// таблица дескрипторов для нереального режима
align 16
descTable dq 0 ;// нулевой декскриптор в GDT
dq 0x00cf93000000ffff ;// 4Gb-дескриптор для регистра FS
newGdt dw $ - descTable ;// размер новой таблицы
gdtBase dd 0 ;// будет указателем на неё.
;//******************
;//******************
start: mov ax,3 ;// очищаем консоль
int 0x10 ;//
;//*****(1) Вычисляем линейную базу новой таблицы GDT *****
xor eax,eax ;//
mov ax,ds ;//
shl eax,4 ;//
add ax,descTable ;//
mov [gdtBase],eax ;//
lgdt fword[newGdt] ;// обновить регистр GDTR!
;//*****(2) Переход в защищённый режим ********************
cli ;// запретить все прерывания
in al,0x70 ;// ..включая NMI
or al,10000000b ;//
out 0x70,al ;//
mov eax,cr0 ;//
or al,1 ;//
mov cr0,eax ;// теперь процессор в P-Mode!
jmp $+2 ;//
mov ax,8 ;// смещение дескриптора в GDT
mov fs,ax ;// записать его в регистр FS
mov eax,cr0 ;//
and al,not 1 ;//
mov cr0,eax ;// процессор вернулся в R-Mode!
jmp $+2 ;//
xor ax,ax ;//
mov fs,ax ;//
in al,0x70 ;// снять запрет с прерываний
and al,not 10000000b
out 0x70,al ;//
sti ;//
;//*****(3) Теперь у нас есть доступ к 4Gb в RM *********
;//***** Читаем регистр "IOAPIC-ID" контролёра **********
mov dx,caption ;//
call Message ;// Un-Real mode OK!!!
mov edi,[apicIndex] ;// EDI = порт-индекса для записи
mov esi,[apicData] ;// ESI = порт-данных для чтения
mov byte[fs:edi],0 ;//<-- индекс нуль = регистр "IOAPIC-ID"
mov eax,[fs:esi] ;// считать его в EAX
shr eax,24 ;// оставить биты [31:24]
mov ebx,10 ;// вывод в 10-тичном
call Hex2Asc ;// показать "IOAPIC-ID"!
;//***** Регистр "IOAPIC-Ver" ******************************************
;// в нём лежит версия и число 64-битных записей, начиная с индекса 0х10
mov dx,ioVersion ;//
call Message ;//
mov edi,[apicIndex] ;//
mov esi,[apicData] ;//
mov byte[fs:edi],1 ;//<-- индекс 1
mov eax,[fs:esi] ;// считать регистр "IOAPIC-Ver" в EAX
push eax ;// запомнить..
and eax,0xff ;// оставить только мл.байт [7:0]
mov ebx,16 ;// выводить будем в HEX
call Hex2Asc ;// Показать версию IOAPIC
mov dx,ioTotalPin ;// Кол-во входных пинов
call Message ;// ..лежат в битах [24:16]
pop eax ;//
shr eax,16 ;// вправо на 16
and eax,0xff ;// оставить только мл.байт
mov ebx,10 ;// вывод в 10-тичном
call Hex2Asc ;//
;//*****(4) Циклический обход всех регистров "IOAPIC.REDIRECTION.TABLE"
;// это макс.240 64-битных регистров начиная с индекса 0х10 ***********
mov dx,apicRedTbl ;//
call Message ;//
mov edi,[apicIndex] ;//
mov esi,[apicData] ;//
@cycle: inc [irq] ;// ..следующий IRQ
mov al,[index] ;// взять в AL индекс из переменной
mov byte[fs:edi],al ;// записать его в регистр-индекса IOAPIC
mov eax,[fs:esi] ;// считать очередной регистр в EAX
cmp al,-1 ;// в его битах [7:0] лежит вектор
je @fuck ;// ..пропустить, если это 0хFF
push eax eax eax eax ;// запомнить для вывода данных на консоль!
mov dx,intIrq ;// выводим номер пина IRQ
call Message ;//
movzx eax,[irq] ;// ..(он лежит в переменной)
mov ebx,16 ;// вывод в HEX
call Hex2Asc ;//
;// Вектор по номеру IRQ
mov dx,intVect ;//
call Message ;//
pop eax ;//
and eax,0xff ;// он лежит в младшем байте [7:0]
mov ebx,16 ;//
call Hex2Asc ;//
;// Маска прерывания
mov dx,intMask ;//
call Message ;//
pop eax ;//
bt eax,16 ;// лежит в бите[16]
call PrintBool ;// вывести её в лог.виде 0/1
;// Режим доставки может иметь 6-вариантов
mov dx,intMode ;//
call Message ;//
pop ebx ;// ..лежит в битах [10:8]
shr ebx,8 ;// сдвинуть вправо на 8-бит
and ebx,111b ;// оставить только 3 мл.бита
push esi ;//
mov esi,modeTable ;// ESI указывает на таблицу
mov ecx,6 ;// ..всего элементов (режимов) в ней
@@: lodsw ;// АХ = очередной режим из таблицы
cmp al,bl ;// сравнить со-считанным из регистра значением
je @found ;// если нашли..
add esi,2 ;// иначе: сл.элемент в таблице
loop @b ;// промотать цикл(@@) ECX-раз..
mov dx,unknown ;// прокол! нет совпадений
call Message ;// выводим мессагу
jmp @f ;// ..и на нижнюю метку(@@).
@found: mov dx,[si] ;// Нашли - взять адрес мессаги.
call Message ;// на консоль её.
@@: pop esi ;//
;// Получатель прерывания CPU или LAPIC
mov dx,intDest ;//
call Message ;//
pop eax ;//
bt eax,11 ;// зарыт в бите [11]
mov dx,phy ;//
jc @f ;//
mov dx,logical ;//
@@: call Message ;//
;// Сделали очередной круг..
;// переходим к сл.регистру "IOAPIC.REDIRECTION.TABLE"
@fuck: add [index],2 ;// увеличить индекс на 2
dec [counter] ;// уменьшить счётчик кол-ва регистров
jnz @cycle ;// Повторить цикл!
@exit: xor ax,ax ;// ждём клаву..
int 16h ;//
int 20h ;// на выход!
;//***** П Р О Ц Е Д У Р Ы *************************
Message: mov ah,9
int 21h
retn
;//---------
PrintBool:
mov al,'1'
jc @f
dec al
@@: int 29h
retn
;//---------
Hex2Asc:
xor ecx,ecx
isDiv: xor edx,edx
div ebx
push edx
inc ecx
or eax,eax
jnz isDiv
isOut: pop eax
cmp al,9
jle noHex
add al,7
noHex: add al,30h
int 29h
loop isOut
retn
Обратите внимание, что в данном случае векторы всех прерываний замаскированы логической единицей. Значит при включении машины как биос так и новороченый EFI используют устаревший контролёр PIC с его 15- входами. Снимает маску и включает IOAPIC позже уже загрузчик операционной системы, когда переходит в защищённый режим и выстраивает свою таблицу IDT – Interrupt Descriptor Table. Кроме того на х64 видно, что контролёр имеет 120 входных пинов IRQ, в то время-как на х32 их всего 24.
Отметим, что прерывания IRQ посылают только физические устройства, поэтому они относятся к классу "аппаратных". Есть ещё и программные прерывания, например
INT-2Eh
(вызов системных сервисов Win), или INT-03h
(брекпоинт) – они вызываются нашим кодом, поэтому к контролёру IOAPIC не имеют никакого отношения.Шина APIC и векторы прерываний
В масштабах истории, моногамный брак APIC+LAPIC появился относительно недавно. Во-времена господства древнего контролёра PIC, взаимосвязь его с CPU осуществлялась по системной пар-шине FSB. В современной-же архитектуре, IOAPIC общается с локальным контролёром посредством специально выделенной для этих целей трёхпроводной последовательной шины 3-Ware-APIC (см.рис.1). Этот тоннель позволяет освободить системную шину DMI от проблем обслуживания прерываний.
По двум из трёх сигнальных линий 3-Ware-APIC гуляет 64-битный трафик в виде данных регистра IOAPIC.RED.TABLE, а третья линия – сигнал тактовой частоты. Протокол новой шины поддерживает механизм арбитража ядер по приоритетам 0-15, а сам приоритет динамически меняется после каждой передачи сообщений.
Получив 64-битные данные, LAPIC выделяет из них вектор прерывания, который зашит в младшем байте пакета (см.последнюю таблицу). Теперь логика умножает этот вектор на 8 (размер дескриптора в таблице IDT) и таким образом получает адрес обработчика возникшего прерывания для последующего его вывоза.
В системной таблице IDT собраны 8-байтные дескрипторы прерываний - всего их 256. На эту таблицу указывает 6-байтный регистр
IDTR
процессора, в младшем слове которого лежит размер таблицы, а в старшем двойном слове – указатель на неё. Отладчик WinDbg на команду !pcr (Processor Control Region) отзывается списком системных ресурсов, где среди прочих будет и указатель на IDT. Дальше можно запросить уже дамп самой таблицы командой dq address (dump qword) – вот пример:На этом рисунке, внутри зелёного блока видим дескрипторы прерываний, каждый из которых имеет свой вектор (порядковый номер в данной таблице). Например по вектору(0) лежит дескриптор со-значением
0x804d8e00'0008f350
и т.д.. Как-правило формат большинства дескрипторов совпадает, однако из общего пула здесь можно выделить два – это начиная с нуля второй (немаскируемое прерывание NMI) со-значение 0x00008500'0058113e
, и дескриптор по вектору #8 (Double Fault – двойной отказ, исключение #DF). Они из другой оперы и в данном случае нам не интересны.Процессоры х86 делят дескрипторы на три типа: для шлюзов прерываний Int, ловушек Trap и задач Task.
Мы остановимся лишь на шлюзе-прерываний, который имеет тип
0хЕ
(выделен красным). В библии свидетелей интела том.3 есть битовая карта с его описанием:Так из дескриптора мы вытащили указатель на функцию прерывания, и командой "Unassembler" можно запросить код его обработчика:
Код:
lkd> u 804df350
nt!KiTrap00:
804df350 6a00 push 0
804df352 66c74424020000 mov word ptr [esp+2],0
804df359 55 push ebp
804df35a 53 push ebx
804df35b 56 push esi
804df35c 57 push edi
804df35d 0fa0 push fs
804df35f bb30000000 mov ebx,30h
804df364 8ee3 mov fs,bx
804df366 648b1d00000000 mov ebx,dword ptr fs:[0]
804df36d 53 push ebx
...
WinDbg имеет спец/расширение для просмотра таблицы IDT – запрашивается оно командой !idt (просмотр списка только активных векторов), или она-же с параметром -a (вывод полного списка 256, включая зарезервированные исключения с векторами 0..1Fh):
Код:
lkd> !idt -a
Dumping IDT:
00: 804df350 nt!KiTrap00
01: 804df4cb nt!KiTrap01
02: Task Selector = 0x0058
03: 804df89d nt!KiTrap03
04: 804dfa20 nt!KiTrap04
05: 804dfb81 nt!KiTrap05
06: 804dfd02 nt!KiTrap06
07: 804e036a nt!KiTrap07
08: Task Selector = 0x0050
09: 804e078f nt!KiTrap09
0a: 804e08ac nt!KiTrap0A
0b: 804e09e9 nt!KiTrap0B
0c: 804e0c42 nt!KiTrap0C
0d: 804e0f38 nt!KiTrap0D
0e: 804e164f nt!KiTrap0E
0f: 804e197c nt!KiTrap0F
10: 804e1a99 nt!KiTrap10
11: 804e1bce nt!KiTrap11
12: Task Selector = 0x00A0
13: 804e1d34 nt!KiTrap13
14: 804e197c nt!KiTrap0F
15: 804e197c nt!KiTrap0F
16: 804e197c nt!KiTrap0F
17: 804e197c nt!KiTrap0F
18: 804e197c nt!KiTrap0F
19: 804e197c nt!KiTrap0F
1a: 804e197c nt!KiTrap0F
1b: 804e197c nt!KiTrap0F
1c: 804e197c nt!KiTrap0F
1d: 804e197c nt!KiTrap0F
1e: 804e197c nt!KiTrap0F
1f: 80711fd0 hal!HalpApicSpuriousService
20: 00000000
В процессе формирования таблицы IDT нужно придерживаться определённых правил..
Во-первых, таблица должна быть заполнена полностью, т.е. состоять из всех 256 дескрипторов. Если вектор не используется, отсылаем его к заглушке
IRET
(Interrupt Return). Во-вторых, первые 32-вектора процессор рассчитывает получить в своё распоряжение и мы не должны препятствовать ему в этом. Например при ошибке деления на нуль
#DE
, он на аппаратном уровне передаёт управление в IDT по вектору(0), где и должен лежать указатель на соответствующий обработчик. Поэтому векторы в диапазоне 00..1Fh
процессор забирает под всякого рода свои исключения, а пользовательские – должны начинаться с 20h
и до FFh
(см.лог выше).Таймер Local-APIC
Частота = системная шина | счётчик = 32 бит
Реакция локального APIC на обработку аппаратных прерываний достаточно прозрачна. Если в двух словах, то внешний IOAPIC получает IRQ от устройства Х, и по цепочке пересылает в LAPIC связанный с ним 64-битный свой регистр IOAPIC.RED.TABLE. Дальше LAPIC извлекает из этих 64-бит вектор-прерывания и ставит его в очередь "DPC-Queue" до лучших времён. Позже, векторы вываливаются из очереди и по ним управление передаётся в системную таблицу IDT, от куда и происходит вызов соответствующего обработчика.
Однако ядро процессора имеет и свои/внутренние устройства контроля – всего их 6, в числе которых и некая пародия на таймер. Для обслуживания этих источников, в LAPIC предусмотрена таблица LVT – Local Vector Table. Это закрытая для всех зона и существует она как самостоятельный механизм. LVT состоит из шести 32-битных регистров, битовая маска которых представлена в таблице ниже:
Априори, все
Ссылка скрыта от гостей
отображаются на память по адресу 0xFEE00000
, хотя для уверенности можно считать его базу с модельно-специфичного регистра MSR-0x1b
. Как видно из этой таблицы, встроенный таймер характеризует регистр по смещению Base+0x320. В младший байт этого регистра мы должны зашить вектор-прерывания в системной таблице IDT. Бит[12] позволяет узнать, обработал-ли процессор наше прерывание, или на текущий момент поставил его в очередь DPC. Единичное значение бита[16] накладывает маску на вектор (т.е. запрещает прерывание от таймера), а бит[17] задаёт режим работы счётчика – однократный или циклический.Кроме регистра LVT, локальный таймер имеет ещё три регистра, в которых определяются уже его свойства. Он тактируется частотой системной шины FSB/DMI, причём не эффективной (с применением множителя), а реальной частотой от клокера. Как-правило эта частота имеет начальный порог 100/133 MHz и дальше по возрастающей, в зависимости от архитектуры и модели чипсета. Чтобы угомонить разбушевавшийся таймер, можно применить к этой частоте локальный делитель, значение которого указывается в регистре Base+0x3E0 (см.таблицу ниже). Делитель может принимать значение степени двойки до 128, т.е. 1,2,4,8,16,32,64 и 128.
Теперь в регистре Base+0x380 остаётся указать начальное значение счётчика, от которого со-скорость частоты шины он будет стремиться к нулю. Если битом[17] таймер установлен на периодичность (а именно в таком режиме работают таймеры всех протестированных мною систем), то при каждом достижении счётчиком нуля, таймер будет генерить прерывание, а процессор будет передавать управление по указанному нами вектору, в таблицу IDT.
Интересным моментом тут является то, что если инициализировать счётчик макс.значением
0xFFFFFFFF
, процессор проигнорирует его и принудительно выставит в нём значение частоты системной шины FSB/DMI в герцах. Соответственно если делитель таймера выставить на 1, то в регистре 0x380
получим частоту системной шины – демо/пример ниже подтверждает эту теорию.Программирование таймера LAPIC представляет собой проблему как в защищённом Win, так и в реальном досе – этому есть несколько причин. Ну с виндой всё понятно.. она не даёт нам доступа к памяти выше
0x7FFFFFFF
(нужно писать драйвер). Поэтому придётся химичить в реальном режиме, но и тут нас поджидают неприятности.. Дело в том, что для своей работы LAPIC требует защищённый режим с таблицей дескрипторов прерываний IDT. А раз так, значит для простой задачи нужно будет писать огромное кол-во обработчиков, чтобы была возможность элементарно выводить инфу на экран, пользоваться клавиатурой, обрабатывать исключения и ещё куча всего. При желании конечно-же можно перекроить под линейные адреса готовые обработчики реального режима, но для обычного демо/примера такая овчинка никак не стоит выделки, поэтому мы поступим иначе..
Если мастдай не хочет пускать нас в своё кернел-пространство, то придётся вытаскивать его контрабандным путём, тупо сбросив 1К-байтный дамп памяти
0xFEE00000
в бинарник. Пусть мы при этом лишаемся возможности записи, зато сможем прочитать регистры и сделать хоть какие-то выводы. Раньше, прямо из юзера мы могли прочитать ядерную память открыв через CreateFile() секцию \Device\PhysicalMemory. Однако начиная с Win-2003 системные сторожа запретили этот финт, выставив ему флаг при открытии KERNEL_ONLY.
Чтобы проблема чтения регистров не превратилась в проблему доступа к памяти, мы выберем самый простой путь и воспользуемся сторонней утилитой
Ссылка скрыта от гостей
Двумя кликами мыши она позволит сбросить в файл любой регион системного пространства, а мы потом подключим этот файл к своей программе, и таким образом получим псевдо/регистры контролёра LAPIC. Последовательность действий представлена на рис.ниже:После нажатия пимпы ОК мы получим бинарник с именем MFEE00000.bin – это готовый слепок памяти. Вариант удобен тем, что позволяет снять дамп с разных машин и сравнить значения их регистров LAPIC. Напомню, что база (стартовый адрес) у всех будет одинаковой. Теперь, для подключения внешних файлов, ассемблер FASM имеет директиву file, аргументом которой является только имя файла. В своей демке, я выделил под этот бинарник отдельную секцию, чтобы он не путался среди остальных данных. Вот пример:
C-подобный:
format pe console
include 'win32ax.inc'
entry start
;//-----------
.data
caption db 13,10,' Local APIC info v.01 '
db 13,10,' ==================================='
db 13,10,' Base address...........: 0xFEE00000'
db 13,10,' LAPIC-ID.............: %d',0
lapicVer db 13,10,' LAPIC Version........: %d'
db 13,10,' LAPIC LVT entries....: %d',0
lapicLVT db 13,10
db 13,10,' LVT registers.: Vector | Mask'
db 13,10,' ---------------------+-----'
db 13,10,' Sensor......: 0x%02X | %d',0
perfMon db 13,10,' PerfMon.....: 0x%02X | %d',0
lInt0 db 13,10,' LINT0.......: 0x%02X | %d',0
lInt1 db 13,10,' LINT1.......: 0x%02X | %d',0
timer db 13,10,' Timer.......: 0x%02X | %d',0
lapicTimer db 13,10
db 13,10,' LAPIC Timer registers..'
db 13,10,' Timer divide.........: %d',0
tmrInit db 13,10,' Timer initial count..: %d',0
tmrCounter db 13,10,' Timer current counter: %d',0
tmrFreq db 13,10,' Timer frequency......: %d.%d MHz',0
buff db 0
;//===================================
section '.lapic' data readable ;// секиця под образ регистров LAPIC
lapicBase: ;// метка для обращения
file 'XP_FEE00000.bin' ;// зашиваем бинарник в свое тело!
;//===================================
.code
start:
;//*****(0) LAPIC ID Register. Смещение = 0х20 *****
mov eax,[lapicBase+0x20] ;//
shr eax,24 ;// лежит в битах [31:24]
cinvoke printf,caption,eax ;//
;//*****(1) LAPIC Ver Register. Смещение = 0х30 *****
mov eax,[lapicBase+0x30] ;//
mov ebx,eax ;//
and eax,0xff ;// оставить только мл.байт (версия)
shr ebx,16 ;// биты[24:16] - кол-во элементов в LVT
inc bl ;//
cinvoke printf,lapicVer,eax,ebx
;//*****(2) LVT Thermal Sensor. Смещение = 0х330 *****
mov eax,[lapicBase+0x330] ;//
call GetVectorMask ;// вызвать процедуру (см.ниже)
cinvoke printf,lapicLVT,eax,edx
;//*****(3) LVT PerfMon. Смещение = 0х340 *****
mov eax,[lapicBase+0x340] ;//
call GetVectorMask ;//
cinvoke printf,perfMon,eax,edx
;//*****(4) LVT LINT0. Смещение = 0х350 *****
mov eax,[lapicBase+0x350] ;//
call GetVectorMask ;//
cinvoke printf,lInt0,eax,edx
;//*****(5) LVT LINT1. Смещение = 0х360 *****
mov eax,[lapicBase+0x360] ;//
call GetVectorMask ;//
cinvoke printf,lInt1,eax,edx
;//*****(6) LVT Timer. Смещение = 0х320 *****
mov eax,[lapicBase+0x320] ;//
call GetVectorMask ;//
cinvoke printf,timer,eax,edx
;//******************************************
;//*****(7) LAPIC Timer Registers ***********
;//******************************************
;//-- Делитель таймера
;// он лежит россыпью в битах 3,1,0
;// т.е. из 4-х мл.бит нужно вырезать второй
mov eax,[lapicBase+0x3e0]
and eax,1111b ;// оставить мл.тетраду
mov ebx,eax ;// ..(запомнить в ЕВХ)
shr bl,1 ;// сдвинуть на 1 вправо
and bl,100b ;// оставить только второй бит
and al,11b ;// в AL оставить 2 младших
or bl,al ;// в битах[2:0] BL получили код делителя
;// теперь из кода нужно получить число от 1 до 128.
mov eax,2 ;// искать начнём с делителя 2
mov ecx,7 ;// всего вариантов..
@@: cmp bh,bl ;// перебор возможных
je @f ;// если нашли
shl al,1 ;// иначе: степень двойки
inc bh ;// сл.вариант..
loop @b ;// промотать ЕСХ-раз
mov eax,1 ;// код 111b соответствует делителю 1.
@@: push eax ;// запомнить делитель!
cinvoke printf,lapicTimer,eax
;//-- Счётчик инициализации
;// как-правило он равен частоте шины FSB/делитель
mov eax,[lapicBase+0x380] ;//
push eax ;// запомнить для получения частоты таймера!
cinvoke printf,tmrInit,eax
;//-- Текущий счётчик
mov eax,[lapicBase+0x390]
cinvoke printf,tmrCounter,eax
;//-- Частота таймера
pop eax ebx ;// ЕАХ= счётчик инициализации, ЕВХ= делитель
mul ebx ;// вычислить произведение!
mov ebx,1000000 ;// перевести герцы в MHz
cdq ;// ...
div ebx ;// ...
cinvoke printf,tmrFreq,eax,edx
cinvoke gets,buff ;// ждать клаву..
cinvoke exit,0 ;// на выход!
;//----------- П Р О Ц Е Д У Р А -----------
;// на входе принимает регистр с таблицы LVT
proc GetVectorMask ;//
mov ebx,eax ;//
and eax,0xff ;// ЕАХ = вектор прерывания
mov edx,1 ;//
bt ebx,16 ;// проверить бит[16]
jc @f ;// если взведён..
dec dl ;// иначе: DL -1
@@: ret ;// EDX = маска в лог.виде 0/1
endp
;//-----------
section '.idata' import data readable
library msvcrt,'msvcrt.dll'
import msvcrt, printf,'printf',exit,'exit',gets,'gets'
Этой прожке я вскармливал дампы двух/своих машин – один сделал на семёрке, другой на хрюше. Обратите внимание, что в обоих случаях, системы вообще не используют таймер LAPIC, т.к. векторы всех локальных прерываний в таблице LVT замаскированы – в активном состоянии находится только монитор производительности PerfMon. Однако таймер продолжает работать в холостую, а значит ОС всё-же обращается иногда к нему.
Делитель опорной частоты выставлен в единицу (см.жёлтый блок), зато значение инициализации счётчика разное. Запустив любой софт по сбору информации о железе можно обнаружить, что Init совпадает с частотой системной шины Bus-Speed. Тогда по логике вещей, произведение делителя и значения инициализации должно возвращать рабочую частоту таймера, хотя при других делителях эту теорию проверить мне так и не удалось.
Судя по регистрам LVT, к таймеру привязан вектор
0xFD
, а к монитору 0xFE
. Чтобы убедиться, так-ли это на самом деле, можно в отладчике WinDbg и запросить таблицу IDT – в самом хвосте списка должны будут лежать указатели, на обслуживающие эти прерывания функции. И точно.. один ProfileInterrupt (профилирование кода по таймеру), а другой PerormanceMonitor под кличкой PerfInterrupt:Заключение
В эпилоге темы хотелось-бы отметить, что у каждого ядра процессора своя память и свой контролёр LAPIC. Соответственно базовый адрес их регистров
0xFEE00000
будет одинаковым для всех ядер. На самом деле это иллюзия, т.к. мы имеем дело с виртуальной памятью, и у одного адреса фреймы физической DRAM-памяти будут разными. Другими словами, если прочитать счётчик таймера одного ядра и сравнить его с счётчиком соседнего ядра, то с вероятность 99% значения будут разными, хотя адрес регистров совпадает. Это-же относится и к выключенному состоянию таймера – на ядре(0) он может быть замаскирован, а на ядре(1) включён.
Последнее редактирование: