Skip to content
Snippets Groups Projects
Select Git revision
  • 2023ss default protected
  • 2022ss
  • 2021ss
  • 2020ss
  • 2019ss
  • 2018ss
  • 2017ss
  • 2016ss
  • 2015ss
  • 2014ss
10 results

bs-20210521.txt

Blame
  • Forked from Peter Gerwinski / bs
    99 commits behind the upstream repository.
    bs-20210521.txt 12.61 KiB
    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?