diff --git a/20210521/bs-20210521.txt b/20210521/bs-20210521.txt new file mode 100644 index 0000000000000000000000000000000000000000..ce1cc9236fe2cdbcbbe0e0fa5e5f9202d19a409d --- /dev/null +++ b/20210521/bs-20210521.txt @@ -0,0 +1,309 @@ +Bedeutung von "%rip", "%rax" usw. 21.05.2021, 12:04:22 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Quellen: +https://softwareengineering.stackexchange.com/questions/127668/what-does-the-r-in-x64-register-names-stand-for +https://stackoverflow.com/questions/10995349/what-does-the-r-stand-for-in-rax-rbx-rcx-rdx-rsi-rdi-rbp-rsp + +1. Quelle: + + It means register, and it isn't all for historical reasons. + + The historical part is that Intel got itself into the habit of enumerating + registers with letters with the 8008 (A through E plus H and L). That scheme + was more than adequate at the time because microprocessors had very few + registers and weren't likely to get more, and most designs did it. The + prevailing sentiment then was that software would be rewritten for new CPUs as + they appeared, so changing the register naming scheme between models wouldn't + have been a big deal. Nobody foresaw the 8088 evolving into a "family" after + being incorporated into the IBM PC, and the yoke of backward compatibility + pretty much forced Intel into having to adopt schemes like the "E" on 32-bit + registers to maintain it. + + The non-historical part is all practical. Using letters for general-purpose + registers limits you to 26, fewer if you weed out those that might cause + confusion with the names of special-purpose registers like the program counter, + flags or the stack pointer. + + I don't have a source to confirm it, but I suspect the choice of R as a prefix + and the introduction of R8 through R15 on 64-bit CPUs signals a transition to + numbered registers, which have been the norm among 32-bit-and-larger + architectures not derived from the 8008 for almost half a century. IBM did it + in the 1960s with the 360 and has been followed by the PowerPC, DEC Alpha, + MIPS, SPARC, ARM, Intel's i860 and i960 and a bunch of others that are + long-forgotten. + + You'll note that the existing registers would fit nicely into R0 through R7 if + they existed, and it wouldn't surprise me a bit if they're treated that way + internally. The existing long registers (RAX/EAX/AX/AL, RBX/EBX/BX/BL, etc.) + will probably stay around until the sun burns out. + +rax/eax/ax/al = "accumulator" +rbx/ebx/bx/bl = "base" +rcx/ecx/cx/cl = "counter" +rdx/edx/dx/dl = "data" + +rsi/esi/si = "source index" (64/32/16 Bit) +rdi/edi/di = "destination index" (64/32/16 Bit) + +16-Bit-Assembler: + + mov si <-- "Hello, world!\n" (Zeiger auf String) + mov di <-- Adresse einer Variablen (String dorthin kopieren) + mov cx <-- 14 (Länge) + rep movsb (Kopierschleife in einem einzigen Assembler-Befehl mit Präfix) + +"CISC" - "Complex Instruction Set Computing"; z.B. x86-Architektur +"RISC" - "Reduced Instruction Set Computing"; z.B. i860, Atmel, ARM, MIPS, RISC-V + + Statt "rep movsb": explizite Schleife + +loop: + mov al <-- [si] + mov [di] <-- al + inc si + inc di + dec cx + jnz loop (Jump if Not Zero) + +Intel i486: typischerweise 10 bis 30 Taktzyklen pro Befehl + +Intel i860: bis zu 3 Befehle pro Taktzyklus + Pipeline-Befehle + "delayed branch" + --> Nebenläufigkeit + --> mit 40 MHz ca. 6.5mal so schnell wie i486 mit 66 MHz + +loop: + mov al <-- [si] + mov [di] <-- al + inc si + dec cx + jnz loop (Jump if Not Zero - "delayed") + inc di <-- wird auf jeden Fall noch vor dem Sprung ausgeführt + +Warum kam der Übergang von CISC zu RISC? + + - "rep movsb": etliche Takte pro "Schleifendurchlauf" + - andere Befehle: mehrere Takte pro Befehl + + - RISC: typischerweise 1 Befehl pro Takt + +--> Eine "lange" Schleife in RISC verbraucht weniger + Takte pro Schleifendurchlauf als "rep movsb". +--> RISC ist schneller. + +Systemaufrufe, 21.05.2021, 12:08:00 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +hello-2.c +... +--> _IO_new_do_write (f, ...) +--> new_do_write (f, ...) +--> _IO_SYSWRITE (fp, ...) +--> __libc_write() +Datei sysdeeps/unix/sysv/linux/write.c, Zeile 26 +--> return SYSCALL_CANCEL (write, fd, buf, nbytes); + +b+ │0x7ffff7eb54f0 <__GI___libc_write> lea 0xd61f9(%rip),%rax # 0x7ffff7f8b6f0 │ + >│0x7ffff7eb54f7 <__GI___libc_write+7> mov (%rax),%eax │ + │0x7ffff7eb54f9 <__GI___libc_write+9> test %eax,%eax │ + │0x7ffff7eb54fb <__GI___libc_write+11> jne 0x7ffff7eb5510 <__GI___libc_write+32> │ + │0x7ffff7eb54fd <__GI___libc_write+13> mov $0x1,%eax │ + │0x7ffff7eb5502 <__GI___libc_write+18> syscall │ + │0x7ffff7eb5504 <__GI___libc_write+20> cmp $0xfffffffffffff000,%rax │ + │0x7ffff7eb550a <__GI___libc_write+26> ja 0x7ffff7eb5560 <__GI___libc_write+112> │ + │0x7ffff7eb550c <__GI___libc_write+28> retq │ + │0x7ffff7eb550d <__GI___libc_write+29> nopl (%rax) │ + │0x7ffff7eb5510 <__GI___libc_write+32> push %r12 │ + │0x7ffff7eb5512 <__GI___libc_write+34> mov %rdx,%r12 │ + │0x7ffff7eb5515 <__GI___libc_write+37> push %rbp + +lea: Lade einen Zeiger aus einer konstanten Tabelle in das rax-Register +mov: Lade das, worauf der Zeiger zeigt, in das eax-Register +test: Prüfe, welchen Wert eax hat +jne: Jump if Not Equal = bedingter Sprung, falls ungleich 0 (Vermutung: Fehler) +mov: Lade die Zahl 1 als Parameter für syscall in das eax-Register. 1 steht für "write". +syscall: übergebe an den Kernel --> schreibt "Hello, world!\n" auf den Bildschirm +cmp: Vergleiche den Wert von eax mit einer Zahl +ja: Jump if Above (Prüfen auf Fehler) +retq: Beende die Funktion + +syscall erwartet Parameter in den Prozessor-Registern: + eax: Nummer der Funktion, die aufgerufen werden soll + rsi: Zeiger auf die auszugebenden Daten + (siehe: https://0xax.gitbooks.io/linux-insides/content/SysCall/linux-syscall-1.html) + ebx oder edx: Länge der auszugebenden Daten + (ggf. weitere Register) + +Kernel, 25.05.2017, 17:53:23 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Fragestellung: Wir haben in der glibc beim Maschinenbefehl "syscall" +aufgehört. Wie geht es nun im Kernel weiter? Wer nimmt den Funktions- +aufruf entgegen? + +Suchbegriff: sys_call_table + +--> Es gibt eine Tabelle, die für jeden der durchnumerierten Systemaufrufe + einen Zeiger auf die aufzurufende Funktion enthält. + +--> Der Maschinenbefehl "syscall" ruft eine Funktion innerhalb des + Kernels auf. Wo steht, welche Funktion das ist? + +arch/x86/entry/syscall_64.c: Defnition des Arrays sys_call_table + +Initialisiertes Array von Zeigern auf Funktionen. +Zunächst zeigen alle Zeiger auf die Funktion sys_ni_syscall(), +die lediglich eine Fehlermeldung zurückgibt ("nicht implementiert"). +Danach werden auf diejenigen Funktionen, die es tatsächlich gibt +Zeiger initialisiert. Dies erfolgt durch ein #include der Datei +/usr/src/linux-headers-4.19.0-8-amd64/arch/x86/include/generated/asm/syscalls_64.h +mit vorheriger Definition eines Präprozessor-Macros __SYSCALL(). + + asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = { + /* + * Smells like a compiler bug -- it doesn't work + * when the & below is removed. + */ + [0 ... __NR_syscall_max] = &sys_ni_syscall, + #include <asm/syscalls_64.h> + --> [0] = __x64_sys_read, + [1] = __x64_sys_write, + [2] = __x64_sys_open, + ... + }; + +Vorher wurden mit demselben Trick alle Funktionen vorwärts-deklariert: + + #define __SYSCALL_64(nr, sym, qual) extern asmlinkage long sym(const struct pt_regs *); + #include <asm/syscalls_64.h> + --> extern asmlinkage long __x64_sys_read(const struct pt_regs *); + extern asmlinkage long __x64_sys_write(const struct pt_regs *); + extern asmlinkage long __x64_sys_open(const struct pt_regs *); + ... + #undef __SYSCALL_64 + +Wie können wur nun dafür sorgen, daß der Befehl syscall dieses Array +auch benutzt? + +arch/x86/entry/entry_64.S enthält eine Funktion entry_SYSCALL_64, +die eine Funktion "do_syscall_64()" aufruft +und sich über "USERGS_SYSRET64" beendet. +Dies ist ein Präprozessor-Makro, der zu "swapgs; sysretq;" expandiert +(Definition in include/asm/irqflags.h). + +Woher kommt die Include-Datei in "generated"? +arch/x86/entry/syscalls/Makefile ruft das Shell-Skript syscalltbl.sh auf. +Dieses liest syscall_64.tbl und erzeugt daraus syscall_64.h. + +Damit haben wir die Funktion gefunden, die den Syscall entgegennehmen soll. +Daraus ergeben sich neue Fragen: +1. Wie sorge ich dafür, daß sie tatsächlich bei "syscall" aufgerufen wird? +2. Wie geht es von dort aus weiter bis zu der Tabelle "sys_call_table"? + +zu 1.: +Suche nach "entry_SYSCALL_64" liefert: +arch/x86/kernel/cpu/common.c: wrmsrl(MSR_LSTAR, (unsigned long)entry_SYSCALL_64); + +Was bedeutet "wrmsrl"? +https://sites.google.com/site/masumzh/articles/hypervisor-based-virtualization/compute-virtualization +"As shown below, the content of the IA32_LSTAR MSR (Model Specific Register) +is copied to the instruction pointer register (RIP) [...]" + +Demnach ist das Prozessorregister "IA32_LSTAR MSR" die Einsprungadresse, +zu der der "syscall"-Aufruf springen soll. +Um das Register "IA32_LSTAR MSR" zu setzen, gibt es den Befehl "wrmsrl" +(Write MSR (long = 32 Bit)). +Offensichtlich ist das MSR ein Array, und "IA32_LSTAR" ist der Index. + +zu 2.: +arch/x86/entry/common.c enthält die C-Funcktion do_syscall_64(). +Diese enthält die Zeile "regs->ax = sys_call_table[nr](regs);", also den +Aufruf der Funktion, auf die ein Zeiger in der tabelle "sys_call_table" +an der Stelle "nr" gespeichert ist. + +--> Weiter geht's mit dem Eintrag in sys_call_table, + also mit der Implementation von sys_write(). + +Die Tabelle wird benutzt in der Funktion do_syscall_64(): + + regs->ax = sys_call_table[nr](regs); + +Wo wird die Funktion __x64_sys_write definiert? +Wenn man lange genug sucht, findet man: arch/x86/include/asm/syscall_wrapper.h +Dort steht eine kleine Funktion __x64_sys_write(), die ihrerseits eine Funktion +__se_sys_write() aufruft. + +Der Präprozessor-Makro __SYSCALL_DEFINEx() wird über einen anderen Präprozessor-Makro +SYSCALL_DEFINEx() aufgerufen, der in include/linux/syscalls.h definiert wird +und seinerseits über SYSCALL_DEFINE3() aufgerufen wird, der ebenfalls in +include/linux/syscalls.h definiert wird. + +Wenn man lange genug sucht, findet man: fs/read_write.c +SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf, size_t, count) + +Dort wird eine Funktion definiert, die ihrerseits eine Funktion ksys_write() aufruft. +Diese enthält: + + ret = vfs_write(f.file, buf, count, &pos); + +Hier passiert das eigentliche Schreiben. + +Ebenfalls in fs/read_write.c: + + ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos) + +Diese ruft auf: + + ret = __vfs_write(file, buf, count, pos); + +Ebenfalls in fs/read_write.c: + + ssize_t __vfs_write(struct file *file, const char __user *p, size_t count, loff_t *pos) + +Diese ruft auf: + + if (file->f_op->write) return file->f_op->write(file, p, count, pos); + +:-) + +Jetzt fehlt nur noch: Wie bekommt f_op->write den Wert, den das Modul hinterlegt hat? + +Der Funktion wird ein Parameter "file" übergeben. +Dies ist eine "struct file", die u.a. ein Feld "f_op" enthält, +das auf ein weiteres struct mit Callback-Funktionen zeigt. + +Wie kommen die "richtigen" Callback-Funktionen dort hinein? +Eigentlich müßte dies bei register_chrdev() passieren. +Schauen wir uns daher an, was register_chrdev() eigentlich macht. + +Dies ist definiert in include/linux/fs.h und ruft die Funktion +__register_chrdev() auf. + +Diese wiederum ist definiert in fs/char_dev.c. +Sie alloziert eine cdev-Struktur und speichert darin die fops. +Die cdev-Struktur wird dann mittels cdev_add() in eine Liste eingefügt. +Hierfür ruft cdev_add() die Funktion kobj_map() auf. +Diese Funktion wird in drivers/base/map.c definiert +und legt die cdev-Struktur in einer Hash-Tabelle ab. +Der letzte Parameter "data" ist dabei der Zeiger auf die cdev-Strutur, +die auch die fops enthält. + +Wie kommen nun die fops von der Hash-Tabelle aus in die file-Struktur? + +Vermutung: Dies geschieht beim Öffnen der Datei. + +In fs/open.c gibt es eine Funktion vfs_open(). +Diese sucht den zur Datei gehörenden inode heraus und ruft damit do_dentry_open() auf. + +Theorie der Dateisysteme: Dateien werden durch "inodes" repräsentiert. + +include/linux/fs.h: Definition "inode" + + Die "struct inode" enthält ein Datenfeld "i_rdev", in dem die Major- und Minor-Nr. + der Gerätedatei gemeinsam gespeichert sind. + + --> Sobald wir einen inode kennen, kennen wir Major- und Minor-Nr. der Gerätedatei. + +do_dentry_open() schreibt den inode in die file-Struktur, +holt die fops aus dem inode und legt sie direkt in der file-Struktur ab. + +# Das einzige, was jetzt noch fehlt: Wie kommen die fops in den inode?