Skip to content
Snippets Groups Projects
Commit bafd86cb authored by Peter Gerwinski's avatar Peter Gerwinski
Browse files

Notizen, Beispiele und Screenshots 6.5.2024

parent 5918708b
No related branches found
No related tags found
No related merge requests found
File added
Vom syscall-Befehl bis zum Treiber-Modul, 22.05.2023, 16:11:55
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
arch/x86/entry/entry_64.S:
Assembler-Code für den Einsprung, nachdem das Benutzerprogramm
den syscall-Befehl aufgerufen hat
(einschließlich "trampoline": Code, der ermöglicht, ein Callback
auch mit einer anderen als der vorgesehenen Anzahl von Parametern
aufzurufen)
--> Funktionsaufruf do_syscall_64 (unsigned long a, struct pt_regs *b)
a = Nummer des Syscall-Aufrufs
b = Zeiger auf eine Datenstruktur auf dem CPU-Stack,
die die Registerinhalte des Benutzerprogramms enthält:
Stack-Segment, Stack-Pointer, CPU-Flags, Code-Segment, Instruction Pointer
(wichtig für den Zugriff auf Variable des aufrufenden Programms
sowie für den Rücksprung)
arch/x86/entry/common.c:
if (unlikely(nr >= NR_syscalls))
goto bad;
--> Hinweis an den Compiler, welcher Zweig einer if-Verzweigung
wahrscheinlicher ist, mit dem Ziel, Pipelining möglichst effizient
zu unterstützen
nr = array_index_nospec(nr, NR_syscalls)
--> Mache aus dem Übergebenen Parameter (Nr. des Syscalls)
einen Index für ein Array.
Wenn alles funktioniert hat:
regs->ax = sys_call_table[nr](regs);
--> Aufruf der Funktion, die den Syscall durchführt.
Das Ergebnis des Funktionsaufrufs speichern wir in der Struktur, die
die Registerinhalte des Aufrufers enthält. Der Aufrufer bekommt somit
den Funktionsrückgabewert in seinem ax-Register (genauer: %rax).
sys_call_table ist eine Tabelle mit Zeigern auf Funktionen.
Worauf zeigen diese?
arch/x86/entry/syscalls/syscall_64.tbl:
0 common read __x64_sys_read
1 common write __x64_sys_write
2 common open __x64_sys_open
...
arch/x86/entry/syscalls/syscalltbl.sh:
Dieses Shell-Skript macht aus der o.a. Tabelle eine Datei "syscalls_64.h"
mit Aufrufen eines Präprozessor-Macros:
__SYSCALL_64(0, __x64_sys_read, )
__SYSCALL_64(0, sys_read, )
__SYSCALL_64(0, __x64_sys_read, )
__SYSCALL_64(0, sys_read, )
__SYSCALL_64(1, __x64_sys_write, )
__SYSCALL_64(1, sys_write, )
__SYSCALL_64(2, __x64_sys_open, )
__SYSCALL_64(2, sys_open, )
...
arch/x86/entry/syscall_64.c:
#define __SYSCALL_64(nr, sym, qual) extern asmlinkage long sym(const struct pt_regs *);
#include <asm/syscalls_64.h>
#undef __SYSCALL_64
--> Extern-Deklarationen aller SysCall-Funktionen (Prototypen)
Dadurch möglich: Zeiger auf diese Funktionen zeigen lassen
#define __SYSCALL_64(nr, sym, qual) [nr] = sym,
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>
};
--> Dies ist die Tabelle, ein initialisiertes Array.
Bei jedem Index "nr" steht ein Zeiger auf die Funktion "sym".
Vorher initialisieren wir das ganze Array auf "&sys_ni_syscall"
("not implemented syscall").
Wie sorgen wir nun dafür, daß unser Einsprungpunkt "entry_SYSCALL_64_trampoline"
tatsächlich aufgerufen wird, wenn ein Benutzerprogramm den syscall-Befehl aufruft?
arch/x86/kernel/cpu/common.c:
void syscall_init(void)
{
...
wrmsr(MSR_STAR, 0, (__USER32_CS << 16) | __KERNEL_CS);
if (static_cpu_has(X86_FEATURE_PTI))
wrmsrl(MSR_LSTAR, SYSCALL64_entry_trampoline);
else
wrmsrl(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);
...
}
--> Hier hinterlegen wir die Adresse von SYSCALL64_entry_trampoline
in einem internen Prozessorregister, das genau für diesen Aufruf
zuständig ist.
wrmsrl: In ein solches Register schreiben
MSR_STAR: Nummer des Registers, das dafür zuständig ist, sich die Segmente zu merken
(__KERNEL_CS = Code-Segment des Kernels)
MSR_LSTAR: Nummer des Registers, das dafür zuständig ist, sich das Offset zu merken
Wo ist die Funktion sys_write() denn nun definiert?
include/linux/syscalls.h:
/* fs/read_write.c */
...
asmlinkage long sys_write(unsigned int fd, const char __user *buf,
size_t count);
fs/read_write.c:
ssize_t ksys_write(unsigned int fd, const char __user *buf, size_t count)
{
struct fd f = fdget_pos(fd);
ssize_t ret = -EBADF;
if (f.file) {
loff_t pos = file_pos_read(f.file);
ret = vfs_write(f.file, buf, count, &pos);
if (ret >= 0)
file_pos_write(f.file, pos);
fdput_pos(f);
}
return ret;
}
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
size_t, count)
{
return ksys_write(fd, buf, count);
}
ebenfalls in fs/read_write.c:
ssize_t __vfs_write(struct file *file, const char __user *p, size_t count,
loff_t *pos)
{
if (file->f_op->write)
return file->f_op->write(file, p, count, pos);
else if (file->f_op->write_iter)
return new_sync_write(file, p, count, pos);
else
return -EINVAL;
}
Damit sind wir eigentlich am Ziel.
Noch offen: Wie gelangen die "fops"-Callbacks aus dem Kernel-Modul
in die "struct file"-Datenstruktur?
Dies führt uns zu dem Konzept der inodes (index nodes).
include/linux/fs.h:
struct file {
union {
struct llist_node fu_llist;
struct rcu_head fu_rcuhead;
} f_u;
struct path f_path;
struct inode *f_inode; /* cached value */
const struct file_operations *f_op;
/*
* Protects f_ep_links, f_flags.
* Must not be taken from IRQ context.
*/
spinlock_t f_lock;
enum rw_hint f_write_hint;
atomic_long_t f_count;
unsigned int f_flags;
fmode_t f_mode;
struct mutex f_pos_lock;
loff_t f_pos;
struct fown_struct f_owner;
const struct cred *f_cred;
struct file_ra_state f_ra;
u64 f_version;
#ifdef CONFIG_SECURITY
void *f_security;
#endif
/* needed for tty driver, and maybe others */
void *private_data;
#ifdef CONFIG_EPOLL
/* Used by fs/eventpoll.c to link all the hooks to this file */
struct list_head f_ep_links;
struct list_head f_tfile_llink;
#endif /* #ifdef CONFIG_EPOLL */
struct address_space *f_mapping;
errseq_t f_wb_err;
} __randomize_layout
__attribute__((aligned(4))); /* lest something weird decides that 2 is OK */
...
struct file_operations {
...
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
...
} __randomize_layout;
Dieselbe "struct file_operations" übergeben wir in Kernel-Modulen beim Aufruf
von register_chrdev().
ebenfalls in include/linux/fs.h:
struct inode {
...
dev_t i_rdev;
...
const struct file_operations *i_fop; /* former ->i_op->default_file_ops */
...
union {
struct pipe_inode_info *i_pipe;
struct block_device *i_bdev;
struct cdev *i_cdev;
char *i_link;
unsigned i_dir_seq;
};
...
} __randomize_layout;
--> Ein inode repräsentiert entweder eine Pipe oder ein Block Device oder ein Char Device
oder einen Symlink oder ein Verzeichnis oder eine normale Datei.
include/linux/cdev.h:
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
} __randomize_layout;
include/linux/types.h:
typedef u32 __kernel_dev_t;
...
typedef __kernel_dev_t dev_t;
include/linux/kdev_t.h:
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
Damit haben wir auch gefunden, wie die Datei ihre file_operations
aus dem Kernel-Modul bekommt (nämlich über den inode).
Von "Hello, world!\n" bis zum Kernel, 15.05.2023, 16:20:31
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
printf ("Hello, world!\n"); wird zu puts ("Hello, world!"); optimiert.
Quelltext von puts(): apt-get source glibc
cassini/home/peter/bo/2023ss/bs/20230515/glibc-2.28> find . -name "*puts*.c"
./gshadow/putsgent.c
./gshadow/tst-putsgent.c
./libio/iofputs.c
./libio/iofputs_u.c
./libio/ioputs.c
...
int _IO_puts (const char *str)
weak_alias (_IO_puts, puts)
--> Information für den Linker:
Wenn ein Benutzerprogramm nach "puts()" fragt, gib ihm "_IO_puts()".
include/libc-symbols.h:
/* Define ALIASNAME as a weak alias for NAME.
If weak aliases are not available, this defines a strong alias. */
# define weak_alias(name, aliasname) _weak_alias (name, aliasname)
# define _weak_alias(name, aliasname) \
extern __typeof (name) aliasname __attribute__ ((weak, alias (#name)));
"__attribute__ (( ... ));" ist ein spezieller GCC-Befehl.
Warum 2 Klammern auf und zu?
Wenn ein anderer Compiler diesen Befehl nicht kennt, machen wir:
#define __attribute__ (X)
Mit nur einem Klammerpaar müßten wir zwischen
#define __attribute__ (X)
#define __attribute__ (X,Y)
#define __attribute__ (X,Y,Z)
unterscheiden. Der Präprozessor kennt aber nur eine feste Anzahl von Parametern.
Warum "weak_alias"?
--> Vermeidung von Namenskonflikten.
Wenn das Benutzerprogramm selbst eine Funktion puts() bereitstellt,
hat diese Vorrang vor der in der glibc.
puts() ruft _IO_sputn() auf.
_IO_sputn() ist ein Makro:
libio/libioP.h:
#define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n)
Dies ist wahrscheinlich ein "Hook": Durch Umdefinieren des Makros
kann man auf einfache Weise die Funktion auf eine andere "umleiten".
Dieser ruft einen weiteren Makro auf:
libio/libioP.h:
#define _IO_XSPUTN(FP, DATA, N) JUMP2 (__xsputn, FP, DATA, N)
Dieser ruft einen weiteren Makro auf:
libio/libioP.h:
#define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2)
Dieser ruft eine Funktion auf, verwendet dabei einen weiteren Makro:
libio/libioP.h:
# define _IO_JUMPS_FUNC(THIS) \
(IO_validate_vtable \
(*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS) \
+ (THIS)->_vtable_offset)))
"vtable" ist eine virtuelle Methodentabelle.
_IO_puts() ist eine "Methode" der Datei (hier: stdout).
Vor dem Aufruf der Methode macht glibc eine Laufzeitüberprüfung der
virtuellen Methodentabelle.
libio/libioP.h enthält eine Typendeklaration struct _IO_jump_t
grep -ir " _IO_jump_t" *
libio/fileops.c:
const struct _IO_jump_t _IO_file_jumps libio_vtable =
{
/* Inhalt der vtable ... */
...
JUMP_INIT(overflow, _IO_file_overflow),
...
JUMP_INIT(xsputn, _IO_file_xsputn),
...
JUMP_INIT(write, _IO_new_file_write),
...
}
Die Tabelle heißt "_IO_file_jumps". "libio_vtable" ist ein Macro:
glibc-2.36/libio/libioP.h:#define libio_vtable __attribute__ ((section ("__libc_IO_vtables")))
--> "Compiler-Magie", um diese Variable speziell zu behandeln
Funktion:
size_t
_IO_new_file_xsputn (FILE *f, const void *data, size_t n)
{
...
}
libc_hidden_ver (_IO_new_file_xsputn, _IO_file_xsputn)
Diese ruft gegen Ende eine weitere Funktion auf:
_IO_default_xsputn()
libio/genops.c:
_IO_default_xsputn (FILE *f, const void *data, size_t n)
Diese ruft u.a. einen Makro _IO_OVERFLOW auf.
Dort erfolgt das eigentliche Schreiben.
libio/libioP.h:
#define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)
Dies ist also wieder der Aufruf einer virtuellen Methode.
libio/genops.c:
int
_IO_new_file_overflow (FILE *f, int ch)
{
...
}
libc_hidden_ver (_IO_new_file_overflow, _IO_file_overflow)
Diese ruft u.a. _IO_do_write() auf.
Dort erfolgt das eigentliche Schreiben.
libio/genops.c:
versioned_symbol (libc, _IO_new_do_write, _IO_do_write, GLIBC_2_1);
int
_IO_new_do_write (FILE *fp, const char *data, size_t to_do)
{
return (to_do == 0
|| (size_t) new_do_write (fp, data, to_do) == to_do) ? 0 : EOF;
}
libc_hidden_ver (_IO_new_do_write, _IO_do_write)
static size_t
new_do_write (FILE *fp, const char *data, size_t to_do)
{
size_t count;
if (fp->_flags & _IO_IS_APPENDING)
/* On a system without a proper O_APPEND implementation,
you would need to sys_seek(0, SEEK_END) here, but is
not needed nor desirable for Unix- or Posix-like systems.
Instead, just indicate that offset (before and after) is
unpredictable. */
fp->_offset = _IO_pos_BAD;
else if (fp->_IO_read_end != fp->_IO_write_base)
{
off64_t new_pos
= _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
if (new_pos == _IO_pos_BAD)
return 0;
fp->_offset = new_pos;
}
count = _IO_SYSWRITE (fp, data, to_do);
if (fp->_cur_column && count)
fp->_cur_column = _IO_adjust_column (fp->_cur_column - 1, data, count) + 1;
_IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_buf_base;
fp->_IO_write_end = (fp->_mode <= 0
&& (fp->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
? fp->_IO_buf_base : fp->_IO_buf_end);
return count;
}
new_do_write() ruft u.a. _IO_SYSWRITE() auf.
Dort erfolgt das eigentliche Schreiben.
libio/libioP.h:
#define _IO_SYSWRITE(FP, DATA, LEN) JUMP2 (__write, FP, DATA, LEN)
libio/fileops.h:
ssize_t
_IO_new_file_write (FILE *f, const void *data, ssize_t n)
{
ssize_t to_do = n;
while (to_do > 0)
{
ssize_t count = (__builtin_expect (f->_flags2
& _IO_FLAGS2_NOTCANCEL, 0)
? __write_nocancel (f->_fileno, data, to_do)
: __write (f->_fileno, data, to_do));
if (count < 0)
{
f->_flags |= _IO_ERR_SEEN;
break;
}
to_do -= count;
data = (void *) ((char *) data + count);
}
n -= to_do;
if (f->_offset >= 0)
f->_offset += n;
return n;
}
grep -r __write * liefert sehr viele Implementationen von __write()
für verschiedenste Unix-Kernel (u.a. Hurd, linux, ...).
--> Ab jetzt ist der Quelltext plattformabhängig.
sysdeps/unix/sysv/linux/write.c:
ssize_t
__libc_write (int fd, const void *buf, size_t nbytes)
{
return SYSCALL_CANCEL (write, fd, buf, nbytes);
}
libc_hidden_def (__libc_write)
weak_alias (__libc_write, __write)
sysdeps/unix/sysdep.h:
#define SYSCALL_CANCEL(...) \
({ \
long int sc_ret; \
if (SINGLE_THREAD_P) \
sc_ret = INLINE_SYSCALL_CALL (__VA_ARGS__); \
else \
{ \
int sc_cancel_oldtype = LIBC_CANCEL_ASYNC (); \
sc_ret = INLINE_SYSCALL_CALL (__VA_ARGS__); \
LIBC_CANCEL_RESET (sc_cancel_oldtype); \
} \
sc_ret; \
})
#define INLINE_SYSCALL_CALL(...) \
__INLINE_SYSCALL_DISP (__INLINE_SYSCALL, __VA_ARGS__)
#define __INLINE_SYSCALL_DISP(b,...) \
__SYSCALL_CONCAT (b,__INLINE_SYSCALL_NARGS(__VA_ARGS__))(__VA_ARGS__)
Der Parameter "b" ist "__INLINE_SYSCALL". Dieser ist neu hinzugekommen.
Wir rufen demnach "__INLINE_SYSCALL<n>" auf, wobei <n> für die Anzahl der
Parameter des Syscalls steht.
#define __INLINE_SYSCALL0(name) \
INLINE_SYSCALL (name, 0)
#define __INLINE_SYSCALL1(name, a1) \
INLINE_SYSCALL (name, 1, a1)
#define __INLINE_SYSCALL2(name, a1, a2) \
INLINE_SYSCALL (name, 2, a1, a2)
...
#ifndef INLINE_SYSCALL
#define INLINE_SYSCALL(name, nr, args...) __syscall_##name (args)
#endif
--> Hier erfolgt der Aufruf der Funktion __syscall_write (fd, buf, nbytes).
sysdeps/unix/syscalls.list: Liste aller Systemaufrufe mit Namen, Parametern
sowie dem Namen der Funktion innerhalb der glibc.
Daraus werden dann automatisch die Funktionen generiert.
sysdeps/unix/sysv/linux/x86_64/syscall.S
enthält ein Assembler-Template (".S" steht für Assembler mit Präprozessor)
für den eigentlichen Funktionsaufruf.
Hier also:
__syscall_write (fd, buf, nbytes)
%rdi: Nummer des Syscalls (hier: 1(?) für "write")
%rsi, %rdx, %rcx, ...: Parameter des Syscalls (hier: fd, buf, nbytes)
syscall: ein spezieller Assembler-Befehl für den Aufruf einer Funktion im Kernel
--> Hardware-Unterstützung für den Übergang Kernel<-->Userspace
(früher auf x86-Prozessoren ab dem i386: Software-Interrupt 0x80)
Rückgabewert des Syscalls: %rax
cmpq $-4095, %rax /* Check %rax for error. */
jae SYSCALL_ERROR_LABEL /* Jump to error handler if error. */
Der Sprung erfolgt, wenn %rax zwischen (einschließlich) -1 und -4095 liegt.
Siehe: https://stackoverflow.com/questions/21440403/what-does-the-cmpq-instruction-do
Vorschlag für die verbleibende Zeit der heutigen Lehrveranstaltung (2023):
Einblick in die Benutzung eines Debuggers
anhand eines einfachen Programms.
"Nicht so etwas Schwieriges wie 'Hello, world!'" (ME)
Im Kernel, 06.05.2024, 18:24:25
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
fs/read_write.c:
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf, size_t, count)
--> ruft ksys_write() auf
--> ruft vfs_write() auf
fs/open.c:
Öffnen der Datei: do_dentry_open()
- bekommt einen inode übergeben,
- ordnet einem Datei-struct den inode zu,
- übergibt die fops (Callbacks) aus dem inode an das Datei-struct.
vfs_write(): Aufruf des Callbacks aus dem Kernel-Modul
if (file->f_op->write)
ret = file->f_op->write(file, buf, count, pos);
Die Funktion inode_permission() aus fs/namei.c prüft, ob geeignete
Zugriffsrechte zum Lesen, Schreiben oder Ausführen gegeben sind.
File added
#include <stdio.h>
int main (void)
{
puts ("Hello, world!");
return 0;
}
#include <stdio.h>
int puts (const char *s)
{
printf ("Ätsch.\n");
return 0;
}
int main (void)
{
puts ("Hello, world!");
return 0;
}
cassini/home/peter/bo/2024ss/bs/20240506> gcc -Wall -O hello-03.c -o hello-03
cassini/home/peter/bo/2024ss/bs/20240506> ./hello-03
Speicherzugriffsfehler
cassini/home/peter/bo/2024ss/bs/20240506> gcc -Wall -O0 hello-03.c -o hello-03
cassini/home/peter/bo/2024ss/bs/20240506> ./hello-03
Speicherzugriffsfehler
.file "hello-03.c"
.text
.section .rodata
.LC0:
.string "\303\204tsch."
.text
.globl puts
.type puts, @function
puts:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movq %rdi, -8(%rbp)
leaq .LC0(%rip), %rax
movq %rax, %rdi
call puts@PLT
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size puts, .-puts
.section .rodata
.LC1:
.string "Hello, world!"
.text
.globl main
.type main, @function
main:
.LFB1:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
leaq .LC1(%rip), %rax
movq %rax, %rdi
call puts
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1:
.size main, .-main
.ident "GCC: (Debian 12.2.0-14) 12.2.0"
.section .note.GNU-stack,"",@progbits
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment