オペレーティングシステムなしでPCIデバイスを見つける方法

作業中、ハードウェアとのかなり低レベルの相互作用に定期的に対処する必要があります。 この記事では、対応するデバイスドライバーの識別と読み込みのためのPCIデバイスの問い合わせがどのように発生するかを示したいと思います。



PCIデバイスを操作するための最小ベースとして、マルチブート仕様をサポートするカーネルを使用します。 これにより、独自のブートセクターとローダーを作成する必要がなくなります。 さらに、この問題はすでにインターネットで十分に取り上げられています。 ブートローダーはGRUBになります。 仮想マシンと実マシンの両方から起動するのが便利なので、フラッシュドライブから起動します。 QEMUを仮想マシンとして使用します。 実際のマシンは、USB-HDDからの起動をサポートする通常のBIOS(UEFIではない)を搭載したマシンである必要があります(通常、レガシーUSBサポートオプションがあります)。 動作するには、expect、qemu、grubの各プログラムを備えたUbuntu Linuxが必要です(sudo apt-get installコマンドを使用して簡単にインストールできます)。 使用されるgccは32ビットコードをコンパイルする必要があります。



最初のステップ-マルチブート仕様をサポートするカーネルの作成を検討してください。 GRUBをローダーとして使用する場合、カーネルは3つのファイルから作成されます。

Kernel.c-プログラムのコードとmain()プロシージャを含むメインファイル。

Loader.s -GRUBのマルチブートヘッダーが含まれています。

Linker.ldは、カーネルが配置されるアドレスを具体的に示すldリンカースクリプトです。



Linker.ldコンテンツ:

ENTRY (loader) SECTIONS { . = 0x00100000; .text ALIGN (0x1000) : { *(.text) } .rodata ALIGN (0x1000) : { *(.rodata*) } .data ALIGN (0x1000) : { *(.data) } .bss : { sbss = .; *(COMMON) *(.bss) ebss = .; } }
      
      







リンカスクリプトは、既にコンパイルされたオブジェクトファイルをリンクする方法を示します。 最初の行は、コアのエントリポイントが「loader」というラベルのアドレスになることを示しています。 さらにスクリプトでは、アドレス0x00100000(1Mb)から始まるテキストセクションが配置されることが示されています。 rodata、data、およびbssセクションは0x1000(4Kb)に揃えられ、テキストセクションの後に配置されます。



Loader.sの内容:

 .global loader .set FLAGS, 0x0 .set MAGIC, 0x1BADB002 .set CHECKSUM, -(MAGIC + FLAGS) .align 4 .long MAGIC .long FLAGS .long CHECKSUM # reserve initial kernel stack space .set STACKSIZE, 0x4000 .lcomm stack, STACKSIZE .comm mbd, 4 .comm magic, 4 loader: movl $(stack + STACKSIZE), %esp movl %eax, magic movl %ebx, mbd call kmain cli hang: hlt jmp hang
      
      







ディスクからカーネルイメージをロードした後、GRUBは、ダウンロードされたイメージの最初の8Kbで署名0x1BADB002を探します。 署名は、最初のマルチブートヘッダーフィールドです。 タイトル自体は次のとおりです。

オフセット



種類



フィールド名



ご注意



0



u32



魔法



必要な



4



u32







必要な



8



u32



チェックサム



必要な



12



u32



header_addr



フラグ[16]が設定されている場合



16



u32



load_addr



フラグ[16]が設定されている場合



20



u32



load_end_addr



フラグ[16]が設定されている場合



24



u32



bss_end_addr



フラグ[16]が設定されている場合



28



u32



entry_addr



フラグ[16]が設定されている場合



32



u32



mode_type



フラグ[2]が設定されている場合



36



u32







フラグ[2]が設定されている場合



40



u32



身長



フラグ[2]が設定されている場合



44



u32



深さ



フラグ[2]が設定されている場合





タイトルには、少なくとも3つのフィールド(マジック、フラグ、チェックサム)を含める必要があります。 マジックフィールドは署名であり、上記のように、常に0x1BADB002です。 フラグフィールドには、OS制御の転送時のマシンの状態に関する追加要件が含まれています。 このフィールドの値に応じて、マルチブート情報構造のフィールドのセットが変更される場合があります。 Multiboot Information構造体へのポインタには、ロードされたカーネルへの制御の転送時のEBXレジスタが含まれています。 この例では、フラグフィールドの値は0であり、マルチブートヘッダーは3つのフィールドのみで構成されています。



カーネルに制御を転送する時点で、プロセッサはページングが無効になっている保護モードで動作します。 デバイスの割り込み処理は無効になっています。 GRUBはブート可能なカーネルのスタックを形成しません。これがオペレーティングシステムが最初にすべきことです。 この例では、スタックの下に16KBが割り当てられます。 最後に実行されるアセンブラ文はcall kmain文です。これは、制御をCコード、つまりvoid kmain(void)関数に転送します。



kernel.cの内容:



 #include "printf.h" #include "screen.h" void kmain(void) { clear_screen(); printf(" -- Kernel started! -- \n"); }
      
      







ここにはまだ何も面白いものはありません。 読み込みの観点からは、Cコードのエントリポイントのみに特定の要素が存在する必要があります。インターネットにあるprintf関数の実装を表示するため、およびputchar、clear_screenなどのビデオメモリを操作するためのいくつかの関数が追加されました。



次の簡単なメイクファイルを使用して、カーネルを構築します。

 CC = gcc CFLAGS = -Wall -nostdlib -fno-builtin -nostartfiles -nodefaultlibs LD = ld OBJFILES = \ loader.o \ printf.o \ screen.o \ pci.o \ kernel.o start: all cp ./kernel.bin ./flash/boot/grub/ expect ./grub_install.exp qemu /dev/sdb all: kernel.bin .so: as -o $@ $< .co: $(CC) $(CFLAGS) -o $@ -c $< kernel.bin: $(OBJFILES) $(LD) -T linker.ld -o $@ $^ clean: rm $(OBJFILES) kernel.bin
      
      







これで、ダウンロードできるカーネルができました。 それが本当にロードされることを確認する時が来ました。 GRUBをUSBフラッシュドライブにインストールし、起動時にカーネルをロードするように指示します。 これを行うには、次の手順を実行します。



1. USBフラッシュドライブにパーティションを作成し、GRUBがサポートするファイルシステム(この場合はFAT32ファイルシステム)にフォーマットします。 UbuntuバンドルのDisk Utilityユーティリティを使用して、パーティションを作成できました。







2. USBフラッシュドライブをマウントし、ディレクトリ/ boot / grub /を作成します。 ファイルstage1、stage2、fat_stage1_5を/ usr / libにコピーします。 ディレクトリ/ boot / grub /にテキストファイルmenu.lstを作成して書き込みます

 timeout 5 default 0 title start_kernel root (hd0,0) kernel /boot/grub/kernel.bin
      
      







USBフラッシュドライブにGRUBをインストールするには、grub_install.expファイルのexpectスクリプトを使用します。 その内容:



 log_user 0 spawn grub expect "grub> " send "root (hd1,0)\r" expect "grub> " send "setup (hd1)\r" expect "grub> " send "quit\r" exit 0
      
      







特定のケースでは、他のドライブ番号とデバイス名が可能です。 最終的に、仮想マシンのコンパイルと起動はmake startコマンドで実行する必要があります。 makefileからのこのコマンドは、grub_install.expスクリプトを使用してフラッシュドライブにGRUBをインストールし、プログラムでQEMU仮想マシンを起動します。 すべてが実際のフラッシュドライブから読み込まれるため、QEMU仮想マシンだけでなく、実際のコンピューターからも起動できます。



プログラムで起動されたQEMU仮想マシンは次のとおりです。







それでは、メインタスクに取り掛かりましょう-コンピューターで利用可能なすべてのPCIデバイスをリストします。 PCIは、コンピューター上のデバイスを備えたメインバスです。 マザーボードのよく知られているスロットに挿入される従来のデバイスに加えて、マザーボード自体に配線されたデバイス(いわゆるオンボードデバイス)だけでなく、多数のコントローラー(USBなど)および他のバスへのブリッジ(例:PCI-ISAブリッジ)。 したがって、PCIはコンピューター上のメインバスであり、そこからすべてのデバイスの問い合わせが開始されます。



各PCIデバイスは、256バイトの構造(PCI Configuration Space)に関連付けられており、その設定が配置されています。 デバイスの構成は、最終的にこの構造からのデータの書き込みと読み取りになります。 すべてのPCIデバイスについて、データの読み取りと書き込みは2つの入出力ポートを介して行われます。

0xcf8-PCIアドレスが書き込まれる構成ポート。

0xcfc-構成ポートで指定されたPCIアドレスにデータを読み書きするデータポート。



PCI構成スペースからデータを読み取る場合、デバイスに関する情報を取得し、デバイスへのデータの書き込みを構成できます。



PCIアドレスは、次の32ビット構造です。

ビット31



ビット30から24



ビット23から16



ビット15-11



ビット10-8



ビット7-2



ビット1-0



常に1



予約済み



タイヤ番号



デバイス番号



機能番号



登録番号



常に0





バス番号とデバイス番号は、コンピューター上の物理デバイスを識別します。 物理デバイスには、機能番号で識別されるいくつかの論理デバイスが含まれる場合があります(たとえば、Wi-Fiコントローラーを備えたビデオキャプチャカードには少なくとも2つの機能があります)。



PCI構成スペースは、条件付きで4バイトのレジスタに分割されます。 アクセスされているレジスタ番号は、32ビットPCIアドレスの2番目から7番目のビットに格納されます。 PCIデバイスを記述するPCI構成スペース構造のフィールドは、そのタイプによって異なります。 ただし、すべてのタイプのデバイスについて、構造の最初の4つのレジスタには次のフィールドが含まれます。

登録番号



ビット31から24



ビット23から16



ビット15-8



ビット7-0



0



デバイスID



ベンダーID



1



ステータス



コマンド



2



クラスコード



サブクラス



プログラムIF



リビジョンID



3



ビスト



ヘッダータイプ



レイテンシータイマー



キャッシュラインサイズ





クラスコード -デバイスが実行する機能(ネットワークアダプター、ビデオカードなど)の観点からデバイスのタイプ(クラス)を説明します。

ベンダーID-デバイスメーカーの識別子(世界の各デバイスメーカーは、これらの一意の識別子を1つ以上持っています)。 これらの番号はPCI SIGによって発行されます。

デバイスID-デバイスの一意の識別子(指定されたベンダーIDに固有)。 それらの番号は製造業者によって決定されます。



フィールドDeviceID(DEVと省略)およびVendorID(VENと省略)は、このデバイスに対応するドライバーを決定します。 このために、追加の識別子RevisionID(略してREV)が使用される場合があります。 つまり、Windowsは、コンピューターで新しいデバイスを検出すると、VEN、DEV、およびREVの数字を使用して、Microsoftサーバーを使用してディスクまたはインターネット上で対応するドライバーを検索します。 これらの番号は、デバイスマネージャーでも確認できます。







コンピューターで使用可能なPCIデバイスのリストを取得する最も簡単な方法を実装するコードを検討してください。



 int ReadPCIDevHeader(u32 bus, u32 dev, u32 func, PCIDevHeader *p_pciDevice) { int i; if (p_pciDevice == 0) return 1; for (i = 0; i < sizeof(p_pciDevice->header)/sizeof(p_pciDevice->header[0]); i++) ReadConfig32(bus, dev, func, i, &p_pciDevice->header[i]); if (p_pciDevice->option.vendorID == 0x0000 || p_pciDevice->option.vendorID == 0xffff || p_pciDevice->option.deviceID == 0xffff) return 1; return 0; } void kmain(void) { int bus; int dev; clear_screen(); printf(" -- Kernel started! -- \n"); for (bus = 0; bus < PCI_MAX_BUSES; bus++) for (dev = 0; dev < PCI_MAX_DEVICES; dev++) { u32 func = 0; PCIDevHeader pci_device; if (ReadPCIDevHeader(bus, dev, func, &pci_device)) continue; PrintPCIDevHeader(bus, dev, func, &pci_device); if (pci_device.option.headerType & PCI_HEADERTYPE_MULTIFUNC) { for (func = 1; func < PCI_MAX_FUNCTIONS; func++) { if (ReadPCIDevHeader(bus, dev, func, &pci_device)) continue; PrintPCIDevHeader(bus, dev, func, &pci_device); } } } }
      
      







このコードでは、バス番号とデバイス番号は、読み取りが行われるアドレスに完全に列挙されています。 ヘッダータイプフィールドにPCI_HEADERTYPE_MULTIFUNCフラグが含まれている場合、この物理デバイスはいくつかの論理デバイスを実装します。構成ポートに書き込まれたアドレスでPCIデバイスを検索する場合、関数番号を反復処理する必要があります。 VendorIDの値が正しくない場合、このバスにはこの番号のデバイスはありません。 Qemuでは、このコードは次の結果を出力します。







0x8086はIntelのVendorIDハードウェアです。 0x7000のDeviceIDは、PIIX3 PCI-to-ISA Bridgeデバイスに対応します。 結果のフラッシュドライブからVmWare Workstation 9.0で起動します。 PCIデバイスのリストは非常に長くなり、次のようになりました。







これは、システム内のPCIデバイスの検索がどのように見えるかです。 このアクションは、IBM PCコンピューターで実行されているすべての最新のオペレーティングシステムで実行されます。 オペレーティングシステムの操作における次の手順は、ドライバーを検索し、見つかったデバイスを構成することです。これは、各デバイスに対して個別の方法で既に行われています。



All Articles