Première partie
Le premier article n'a pas encore eu le temps de refroidir, mais j'ai décidé de ne pas vous intriguer et d'écrire une suite.
Ainsi, dans l'article précédent, nous avons parlé de la liaison, du chargement du fichier noyau et de l'initialisation principale. J'ai donné quelques liens utiles, expliqué comment le noyau chargé est situé dans la mémoire, comment les adresses virtuelles et physiques sont comparées au démarrage et comment activer la prise en charge du mécanisme de page. Enfin, le contrôle est passé à la fonction kmain de mon noyau, écrite en Rust. Il est temps de passer à autre chose et de découvrir la profondeur du terrier du lapin!
Dans cette partie des notes, je décrirai brièvement ma configuration Rust, en termes généraux, je parlerai de la sortie des informations en VGA, et en détail de la configuration des segments et des interruptions . Je demande à tous les intéressés sous la coupe, et nous commençons.
Configuration de la rouille
En général, il n'y a rien de particulièrement compliqué dans cette procédure, pour plus de détails vous pouvez contacter le blog Philippe . Cependant, je m'arrêterai à certains moments.
Stable Rust ne prend toujours pas en charge certaines fonctionnalités nécessaires au développement de bas niveau.Par conséquent, pour désactiver la bibliothèque standard et s'appuyer sur Bare Bones, nous avons besoin de Rust tous les soirs. Soyez prudent, une fois après la mise à jour vers la dernière version, j'ai obtenu un compilateur complètement inopérant et j'ai dû revenir au stable le plus proche. Si vous êtes sûr que votre compilateur fonctionnait hier, mais qu'il a été mis à jour et ne fonctionne pas, exécutez la commande en remplaçant la date dont vous avez besoin
rustup override add nightly-YYYY-MM-DD
Pour plus de détails sur le mécanisme, vous pouvez contacter ici .
Ensuite, configurez la plate-forme cible pour laquelle nous allons. J'étais basé sur le blog de Philip Opperman, tant de choses dans cette section lui ont été prises, démontées par des os et adaptées à mes besoins. Philip développe pour x64 dans son blog, j'ai choisi à l'origine x32, donc mon target.json sera légèrement différent. Je l'apporte complètement
{ "llvm-target": "i686-unknown-none", "data-layout": "em:ep:32:32-f64:32:64-f80:32-n8:16:32-S128", "arch": "x86", "target-endian": "little", "target-pointer-width": "32", "target-c-int-width": "32", "os": "none", "executables": true, "linker-flavor": "ld.lld", "linker": "rust-lld", "panic-strategy": "abort", "disable-redzone": true, "features": "-mmx,-sse,+soft-float" }
La partie la plus difficile ici est le paramètre « data-layout ». La documentation LLVM nous indique qu'il s'agit d'options de disposition de données, séparées par «-». Le tout premier caractère «e» est responsable de l'indianité - dans notre cas, il est peu endian, comme l'exige la plate-forme. Le deuxième caractère est m, «distorsion». Responsable des noms des personnages lors de la mise en page. Puisque notre format de sortie sera ELF (voir le script de construction), nous sélectionnons «m: e». Le troisième caractère est la taille du pointeur en bits et ABI (Application binary interface). Tout est simple ici, nous avons 32 bits, donc nous mettons hardiment «p: 32: 32». Viennent ensuite les nombres à virgule flottante. Nous signalons que nous prenons en charge les nombres 64 bits selon ABI 32 avec alignement 64 - "f64: 32: 64", ainsi que les nombres 80 bits avec alignement par défaut - "f80: 32". L'élément suivant est des entiers. Nous commençons avec 8 bits et passons à la plate-forme maximum de 32 bits - «n8: 16: 32». Le dernier est l'alignement de la pile. J'ai même besoin de nombres entiers de 128 bits, alors que ce soit S128. Dans tous les cas, LLVM peut ignorer ce paramètre en toute sécurité, c'est notre préférence.
En ce qui concerne les paramètres restants, vous pouvez jeter un œil à Philip, il explique tout bien.
Nous avons également besoin de cargo-xbuild - un outil qui vous permet de compiler de manière croisée le noyau de rouille lors de la construction sous une plate-forme cible inconnue.
Installez.
cargo install cargo-xbuild
Nous allons le collecter comme ça.
cargo xbuild -Z unstable-options --manifest-path=kernel/Cargo.toml --target kernel/targets/$(ARCH).json --out-dir=build/lib
J'avais besoin d'un manifeste pour le bon fonctionnement de Make, car il démarre à partir du répertoire racine et le noyau se trouve dans le répertoire du noyau.
Parmi les fonctionnalités du manifeste, je ne peux mettre en évidence que crate-type = ["staticlib"] , ce qui donne un fichier pouvant être lié à la sortie. Nous le nourrirons en LLD.
kmain et configuration initiale
Selon les conventions de Rust, si nous créons une bibliothèque statique (ou un fichier binaire «plat»), la racine de la caisse doit contenir le fichier lib.rs, qui est le point d'entrée. Dans celui-ci, à l'aide d'attributs, les fonctionnalités linguistiques sont configurées, et le précieux kmain est également localisé.
Donc, dans la première étape, nous devrons désactiver la bibliothèque std. Cela se fait avec une macro.
#![no_std]
Avec une étape aussi simple, nous oublions immédiatement le multithreading, la mémoire dynamique et les autres délices de la bibliothèque standard. De plus, nous nous privons même de la macro println!, Nous devrons donc l'implémenter nous-mêmes. Je vais vous dire comment faire la prochaine fois.
De nombreux tutoriels quelque part dans ce lieu se terminent par la sortie de "Hello World" et sans expliquer comment vivre. Nous irons dans l'autre sens. Tout d'abord, nous devons définir le code et les segments de données pour le mode protégé, configurer VGA, configurer les interruptions, ce que nous ferons.
#![no_std] #[macro_use] pub mod debug; #[cfg(target_arch = "x86")] #[path = "arch/i686/mod.rs"] pub mod arch; #[no_mangle] extern "C" fn kmain(pd: usize, mb_pointer: usize, mb_magic: usize) { arch::arch_init(pd); ...... } #[panic_handler] fn panic(_info: &PanicInfo) -> ! { println!("{}", _info); loop {} }
Que se passe-t-il ici? Comme je l'ai dit, nous désactivons la bibliothèque standard. Nous annoncerons également deux modules très importants - le débogage (dans lequel nous écrirons à l'écran) et arch (dans lequel toute la magie dépendante de la plateforme vivra). J'utilise la fonction Rust avec des configurations pour déclarer les mêmes interfaces dans différentes implémentations architecturales et les utiliser au maximum. Ici je m'arrête uniquement sur x86 et ensuite on n'en parle que.
J'ai déclaré un gestionnaire de panique complètement primitif, ce dont Rust a besoin. Il sera alors possible de le modifier.
kmain accepte trois arguments et est également exporté en notation C sans distorsion de nom afin que l'éditeur de liens puisse correctement associer la fonction à l'appel de _loader, que j'ai décrit dans l'article précédent. Le premier argument est l'adresse de la table de pages PD, le second est l'adresse physique de la structure GRUB, d'où nous obtiendrons la carte mémoire, le troisième est le nombre magique. À l'avenir, je voudrais implémenter à la fois la prise en charge de Multiboot 2 et mon propre chargeur de démarrage.J'utilise donc un nombre magique pour identifier la méthode de démarrage.
Le premier appel kmain est l'initialisation spécifique à la plate-forme. On rentre. La fonction arch_init se trouve dans le fichier arch / i686 / mod.rs, est publique, spécifique à x86 32 bits et ressemble à ceci:
pub fn arch_init(pd: usize) { unsafe { vga::VGA_WRITER.lock().init(); gdt::setup_gdt(); idt::init_idt(); paging::setup_pd(pd); } }
Comme vous pouvez le voir, pour x86, la sortie, la segmentation, les interruptions et la pagination sont initialisées dans l'ordre. Commençons par VGA.
Initialisation VGA
Chaque tutoriel considère qu'il est de son devoir d'imprimer Hello World, vous trouverez donc comment travailler avec VGA partout. Pour cette raison, j'irai le plus brièvement possible, je me concentrerai uniquement sur les puces que j'ai faites moi-même. Sur l'utilisation de lazy_static je vous enverrai sur le blog de Philippe et ne vous expliquerai pas en détail. const fn n'est pas encore en version, donc les initialisations magnifiquement statiques ne peuvent pas encore être faites. Et nous allons ajouter un verrou de rotation pour qu'il ne se révèle pas être un gâchis.
use lazy_static::lazy_static; use spin::Mutex; lazy_static! { pub static ref VGA_WRITER : Mutex<Writer> = Mutex::new(Writer { cursor_position: 0, vga_color: ColorCode::new(Color::LightGray, Color::Black), buffer: unsafe { &mut *(0xC00B8000 as *mut VgaBuffer) } }); }
Comme vous le savez, le tampon d'écran est situé à l'adresse physique 0xB8000 et a une taille de 80x25x2 octets (largeur et hauteur de l'écran, octet par caractère et attributs: couleurs, scintillement). Puisque nous avons déjà activé la mémoire virtuelle, l'accès à cette adresse se bloquera, nous ajoutons donc 3 Go. Nous déréférençons également un pointeur brut, ce qui n'est pas sûr - mais nous savons ce que nous faisons.
Ce qui est peut-être intéressant dans ce fichier, c'est seulement l'implémentation de la structure Writer, qui permet non seulement d'afficher les caractères dans une rangée, mais aussi de faire défiler, d'aller à n'importe quel endroit de l'écran et d'autres plaisanteries agréables.
Écrivain VGA pub struct Writer { cursor_position: usize, vga_color: ColorCode, buffer: &'static mut VgaBuffer, } impl Writer { pub fn init(&mut self) { let vga_color = self.vga_color; for y in 0..(VGA_HEIGHT - 1) { for x in 0..VGA_WIDTH { self.buffer.chars[y * VGA_WIDTH + x] = ScreenChar { ascii_character: b' ', color_code: vga_color, } } } self.set_cursor_abs(0); } fn set_cursor_abs(&mut self, position: usize) { unsafe { outb(0x3D4, 0x0F); outb(0x3D5, (position & 0xFF) as u8); outb(0x3D4, 0x0E); outb(0x3D4, ((position >> 8) & 0xFF) as u8); } self.cursor_position = position; } pub fn set_cursor(&mut self, x: usize, y: usize) { self.set_cursor_abs(y * VGA_WIDTH + x); } pub fn move_cursor(&mut self, offset: usize) { self.cursor_position = self.cursor_position + offset; self.set_cursor_abs(self.cursor_position); } pub fn get_x(&mut self) -> u8 { (self.cursor_position % VGA_WIDTH) as u8 } pub fn get_y(&mut self) -> u8 { (self.cursor_position / VGA_WIDTH) as u8 } pub fn scroll(&mut self) { for y in 0..(VGA_HEIGHT - 1) { for x in 0..VGA_WIDTH { self.buffer.chars[y * VGA_WIDTH + x] = self.buffer.chars[(y + 1) * VGA_WIDTH + x] } } for x in 0..VGA_WIDTH { let color_code = self.vga_color; self.buffer.chars[(VGA_HEIGHT - 1) * VGA_WIDTH + x] = ScreenChar { ascii_character: b' ', color_code } } } pub fn ln(&mut self) { let next_line = self.get_y() as usize + 1; if next_line >= VGA_HEIGHT { self.scroll(); self.set_cursor(0, VGA_HEIGHT - 1); } else { self.set_cursor(0, next_line) } } pub fn write_byte_at_xy(&mut self, byte: u8, color: ColorCode, x: usize, y: usize) { self.buffer.chars[y * VGA_WIDTH + x] = ScreenChar { ascii_character: byte, color_code: color } } pub fn write_byte_at_pos(&mut self, byte: u8, color: ColorCode, position: usize) { self.buffer.chars[position] = ScreenChar { ascii_character: byte, color_code: color } } pub fn write_byte(&mut self, byte: u8) { if self.cursor_position >= VGA_WIDTH * VGA_HEIGHT { self.scroll(); self.set_cursor(0, VGA_HEIGHT - 1); } self.write_byte_at_pos(byte, self.vga_color, self.cursor_position); self.move_cursor(1); } pub fn write_string(&mut self, s: &str) { for byte in s.bytes() { match byte { 0x20...0xFF => self.write_byte(byte), b'\n' => self.ln(), _ => self.write_byte(0xfe), } } } }
Lors du rembobinage, il suffit de copier des sections de mémoire de la taille de la largeur de l'écran vers l'arrière, en remplissant avec des blancs une nouvelle ligne (c'est ainsi que je fais le nettoyage). Les appels sortants sont un peu plus intéressants - en aucun cas autre que de travailler avec des ports d'E / S, il est impossible de déplacer le curseur. Cependant, nous avons toujours besoin d'entrées / sorties via des ports, ils ont donc été livrés dans un emballage séparé et emballés dans des emballages sécurisés. Sous le spoiler ci-dessous se trouve le code assembleur. Pour l'instant, il suffit de savoir que:
- Le décalage absolu du curseur, et non les coordonnées, s'affiche.
- Vous pouvez émettre vers le contrôleur un octet à la fois
- La sortie d'un octet se produit en deux commandes - nous écrivons d'abord la commande sur le contrôleur, puis les données.
- Le port pour les commandes est 0x3D4, le port de données est 0x3D5
- Tout d'abord, imprimez l'octet inférieur de la position avec la commande 0x0F, puis le haut avec la commande 0x0E
out.asmFaites attention à travailler avec les variables passées sur la pile. Étant donné que la pile commence à la fin de l'espace et réduit le pointeur de pile lors de l'appel de la fonction, pour obtenir des paramètres, un point de retour, etc., vous devez ajouter la taille d'argument alignée avec l'alignement de la pile au registre ESP, dans notre cas 4 octets.
global writeb global writew global writed section .text writeb: push ebp mov ebp, esp mov edx, [ebp + 8] ;port in stack: 8 = 4 (push ebp) + 4 (parameter port length is 2 bytes but stack aligned 4 bytes) mov eax, [ebp + 8 + 4] ;value in stack - 8 = see ^, 4 = 1 byte value aligned 4 bytes out dx, al ;write byte by port number an dx - value in al mov esp, ebp pop ebp ret writew: push ebp mov ebp, esp mov edx, [ebp + 8] ;port in stack: 8 = 4 (push ebp) + 4 (parameter port length is 2 bytes but stack aligned 4 bytes) mov eax, [ebp + 8 + 4] ;value in stack - 8 = see ^, 4 = 1 word value aligned 4 bytes out dx, ax ;write word by port number an dx - value in ax mov esp, ebp pop ebp ret writed: push ebp mov ebp, esp mov edx, [ebp + 8] ;port in stack: 8 = 4 (push ebp) + 4 (parameter port length is 2 bytes but stack aligned 4 bytes) mov eax, [ebp + 8 + 4] ;value in stack - 8 = see ^, 4 = 1 double word value aligned 4 bytes out dx, eax ;write double word by port number an dx - value in eax mov esp, ebp pop ebp ret
Configuration du segment
Nous sommes arrivés au sujet le plus déroutant, mais en même temps le plus simple. Comme je l'ai dit dans un article précédent, l'organisation des pages et des segments de mémoire était mélangée dans ma tête, j'ai chargé l'adresse de la table des pages dans le GDTR et j'ai attrapé ma tête. Il m'a fallu plusieurs mois pour lire suffisamment le matériel, le digérer et pouvoir le réaliser. J'ai peut-être été victime de l'assembleur de manuels de Peter Abel. Le langage et la programmation pour IBM PC »(un excellent livre!), Qui décrit la segmentation pour Intel 8086. Dans ces moments agréables, nous avons chargé les 16 bits supérieurs d'une adresse de vingt bits dans le registre de segment, et c'était l'adresse en mémoire. Il s'est avéré être une cruelle déception qu'en commençant avec i286 en mode protégé, tout soit complètement faux.
Ainsi, la simple théorie est que x86 prend en charge un modèle de mémoire segmentée, car les anciens programmes ne pouvaient sortir qu'au-delà de 640 Ko, puis 1 Mo de mémoire.
Les programmeurs devaient réfléchir à la manière de placer le code exécutable, de placer les données et de maintenir leur sécurité. L'avènement de l'organisation des pages a rendu l'organisation segmentée inutile, mais elle est restée à des fins de compatibilité et de protection (séparation des privilèges pour l'espace noyau et l'espace utilisateur), donc sans elle, ce n'est nulle part. Certaines instructions du processeur sont interdites lorsque le niveau de privilège est inférieur à 0 et l'accès entre les segments de programme et de noyau provoquera une erreur de segmentation.
Faisons-le à nouveau (espérons-le dans le dernier) à propos de la traduction d'adresses
Adresse de ligne [0x08: 0xFFFFFFFF] -> Vérifier les autorisations de segment 0x08 -> Adresse virtuelle [0xFFFFFFFF] -> Tableau de page + TLB -> Adresse physique [0xAAAAFFFF]
Un segment est utilisé uniquement à l'intérieur du processeur, est stocké dans un registre de segment spécial (CS, SS, DS, ES, FS, GS) et est utilisé exclusivement pour vérifier les droits d'exécution de code et de contrôle de transfert. C'est pourquoi vous ne pouvez pas simplement prendre et appeler la fonction noyau depuis l'espace utilisateur. Le segment avec le descripteur 0x18 (j'en ai un, le vôtre est différent) a des droits de niveau 3, et le segment avec le descripteur 0x08 a des droits de niveau 0. Selon la convention x86, pour se protéger contre les accès non autorisés, un segment avec moins de privilèges ne peut pas appeler directement un segment avec grand droits via jmp 0x08: [EAX], mais est obligé d'utiliser d'autres mécanismes, tels que des pièges, des portes, des interruptions.
Les segments et leurs types (code, données, échelles, portes) doivent être décrits dans le tableau des descripteurs globaux GDT, dont l'adresse virtuelle et la taille sont chargés dans le registre GDTR. Lorsque vous passez d'un segment à l'autre (par souci de simplicité, je suppose qu'une transition directe est possible), vous devez appeler l'instruction jmp 0x08: [EAX], où 0x08 est le décalage du premier descripteur valide en octets depuis le début du tableau et EAX est le registre contenant l'adresse de transition. Le décalage (sélecteur) sera chargé dans le registre CS et le descripteur correspondant sera chargé dans le registre fantôme du processeur. Chaque descripteur est une structure à 8 octets. Il est bien documenté et sa description se trouve à la fois sur OSDev et dans la documentation Intel (voir le premier article).
Je résume. Lorsque nous initialisons GDT et exécutons la transition jmp 0x08: [EAX], l'état du processeur sera le suivant:
- GDTR contient une adresse GDT virtuelle
- CS contient la valeur 0x08
- Un handle vers l'adresse [GDTR + 0x08] a été copié dans le registre fantôme CS à partir de la mémoire
- Le registre EIP contient l'adresse du registre EAX
Le descripteur zéro doit toujours être non initialisé et son accès est interdit. Je m'attarderai sur le descripteur TSS et sa signification plus en détail lorsque nous discuterons du multithreading. Ma table GDT ressemble maintenant à ceci:
extern { fn load_gdt(base: *const GdtEntry, limit: u16); } pub unsafe fn setup_gdt() { GDT[5].set_offset((&super::tss::TSS) as *const _ as u32); GDT[5].set_limit(core::mem::size_of::<super::tss::Tss>() as u32); let gdt_ptr: *const GdtEntry = GDT.as_ptr(); let limit = (GDT.len() * core::mem::size_of::<GdtEntry>() - 1) as u16; load_gdt(gdt_ptr, limit); } static mut GDT: [GdtEntry; 7] = [
Et voici l'initialisation, dont j'ai beaucoup parlé plus haut. Le chargement des adresses et des tailles GDT se fait via une structure distincte, qui ne contient que deux champs. L'adresse de cette structure est transmise à la commande lgdt. Dans les registres de segments de données, chargez le descripteur suivant avec un décalage de 0x10.
global load_gdt section .text gdtr dw 0 ; For limit storage dd 0 ; For base storage load_gdt: mov eax, [esp + 4] mov [gdtr + 2], eax mov ax, [esp + 8] mov [gdtr], ax lgdt [gdtr] jmp 0x08:.reload_CS .reload_CS: mov ax, 0x10 ; 0x10 points at the new data selector mov ds, ax mov es, ax mov fs, ax mov gs, ax mov ss, ax mov ax, 0x28 ltr ax ret
Ensuite, tout sera un peu plus facile, mais non moins intéressant.
Interruptions
En fait, il est temps de nous donner l'opportunité d'interagir avec notre cœur (au moins pour voir ce que nous appuyons sur le clavier). Pour ce faire, vous devez initialiser le contrôleur d'interruption.
Digression lyrique sur le style de code.
Grâce aux efforts de la communauté et en particulier de Philip Opperman, la convention d'appel x86-interruption a été ajoutée à Rust, ce qui vous permet d'écrire des gestionnaires d'interruption qui exécutent iret. Cependant, j'ai délibérément décidé de ne pas suivre cette voie, car j'ai décidé de séparer l'assembleur et Rust dans des fichiers différents, et donc des fonctions. Oui, j'utilise déraisonnablement la mémoire de la pile, j'en suis conscient, mais c'est encore du goût. Mes gestionnaires d'interruption sont écrits en assembleur et font exactement une chose: ils appellent presque les mêmes gestionnaires d'interruption écrits en Rust. Veuillez accepter ce fait et soyez indulgents.
En général, le processus d'initialisation des interruptions est similaire à l'initialisation d'un GDT, mais est plus facile à comprendre. D'un autre côté, vous avez besoin de beaucoup de code uniforme. Les développeurs de Redox OS prennent une belle décision, en utilisant tous les plaisirs du langage, mais je suis allé «sur le front» et j'ai décidé d'autoriser la duplication de code.
Selon la convention x86, nous avons des interruptions, mais il existe des situations exceptionnelles. Dans ce contexte, les paramètres pour nous sont pratiquement les mêmes. La seule différence est que lorsqu'une exception est levée, la pile peut contenir des informations supplémentaires. Par exemple, je l'utilise pour gérer le manque de page lorsque je travaille avec un tas (mais tout a son temps). Les interruptions et les exceptions sont traitées à partir de la même table, que vous et moi devons remplir. Il est également nécessaire de programmer le PIC (Programmable Interrupt Controller). Il y a aussi l'APIC, mais je ne l'ai pas encore compris.
En travaillant avec PIC, je ne ferai pas beaucoup de commentaires, car il existe de nombreux exemples sur le réseau pour travailler avec lui. Je vais commencer par les gestionnaires de l'assembleur. Ils sont tous complètement identiques, donc je vais supprimer le code du spoiler.
IRQ global irq0 global irq1 ...... global irq14 global irq15 extern kirq0 extern kirq1 ...... extern kirq14 extern kirq15 section .text irq0: pusha call kirq0 popa iret irq1: pusha call kirq1 popa iret ...... irq14: pusha call kirq14 popa iret irq15: pusha call kirq15 popa iret
Comme vous pouvez le voir, tous les appels aux fonctions Rust commencent par le préfixe «k» - pour la distinction et la commodité. La gestion des exceptions est exactement la même. Pour les fonctions assembleur, le préfixe «e» est sélectionné, pour Rust, «k». Le gestionnaire de défauts de page est différent, mais à ce sujet - dans les notes sur la gestion de la mémoire.
Exceptions global e0_zero_divide global e1_debug ...... global eE_page_fault ...... global e14_virtualization global e1E_security extern k0_zero_divide extern k1_debug ...... extern kE_page_fault ...... extern k14_virtualization extern k1E_security section .text e0_zero_divide: pushad call k0_zero_divide popad iret e1_debug: pushad call k1_debug popad iret ...... eE_page_fault: pushad mov eax, [esp + 32] push eax mov eax, cr2 push eax call kE_page_fault pop eax pop eax popad add esp, 4 iret ...... e14_virtualization: pushad call k14_virtualization popad iret e1E_security: pushad call k1E_security popad iret
Nous déclarons les gestionnaires d'assembleurs:
extern { fn load_idt(base: *const IdtEntry, limit: u16); fn e0_zero_divide(); fn e1_debug(); ...... fn e14_virtualization(); fn e1E_security(); fn irq0(); fn irq1(); ...... fn irq14(); fn irq15(); }
Nous définissons les gestionnaires Rust que nous appelons ci-dessus. Veuillez noter que pour interrompre le clavier, j'affiche simplement le code reçu, que j'obtiens du port 0x60 - c'est ainsi que le clavier fonctionne dans le mode le plus simple. À l'avenir, cela se transformera en un pilote à part entière, j'espère. Après chaque interruption, vous devez envoyer au contrôleur le signal de fin de traitement 0x20, c'est important! Sinon, vous n'obtiendrez pas plus d'interruptions.
#[no_mangle] pub unsafe extern fn kirq0() {
Initialisation de IDT et PIC. À propos de PIC et de son remappage, j'ai trouvé un grand nombre de didacticiels de différents degrés de détail, commençant par OSDev et se terminant par des sites amateurs. Étant donné que la procédure de programmation fonctionne avec une séquence constante d'opérations et de commandes constantes, je donnerai ce code sans autre explication. , 0x20-0x2F , 0x20 0x28, 16 IDT.
unsafe fn setup_pic(pic1: u8, pic2: u8) {
IDTR GDTR — . STI — — , , ASCII- -.
global load_idt section .text idtr dw 0 ; For limit storage dd 0 ; For base storage load_idt: mov eax, [esp + 4] mov [idtr + 2], eax mov ax, [esp + 8] mov [idtr], ax lidt [idtr] sti ret
Postface
, , . setup_pd, . , , , .
- GitLab .
Merci de votre attention!
UPD: 3