OpenSolaris/Illumos has exemplary debugging and tracing facilities, the Modular Debugger (mdb) and DTrace (dtrace). GNU/Linux and other operating systems are catching up, but for now their respective mechanisms (e.g., Linux's Kprobes/SystemTap) haven't achieved the same level of maturity. Please have a (free) distribution of Illumos (e.g., OpenIndiana) installed in a virtual machine (e.g., VirtualBox) by the end of this week. Read http://www.cs.dartmouth.edu/~sergey/cs258/Modular-Debugger-Tutorial.pdf and http://www.cs.dartmouth.edu/~sergey/cs258/DTrace-User-Guide.pdf and experiment with their various commands (see suggestions below). ========= Tutorials and Docs ========== MDB: To examine the running Illumos kernel: "mdb -k". Mdb tutorial: http://www.cs.dartmouth.edu/~sergey/cs258/Modular-Debugger-Tutorial.pdf Somewhat shorter tutorial (but ignore the adb-specific and SPARC-specific detals): http://www.princeton.edu/~unix/Solaris/troubleshoot/adb.html Current docs: http://docs.oracle.com/cd/E19253-01/816-5041/ DTrace: http://www.cs.dartmouth.edu/~sergey/cs258/DTrace-User-Guide.pdf ======================== Useful MDB commands (explained in the above tutorials): ::help Throughout the following, piping the output to grep or less can be very helpful. E.g., "::ps ! grep bash" or "::ps ! less". MDB "embeds" the shell commands after the first !, so you can can use any shell pipes after the initial ! . For example, to list all processes matching 'bash' into a 'less' buffer: "::ps ! grep bash | less" ::dcmds -- list composite commands to present and walk various kinds of kernel's data structures ::ps -- list all processes ::nm -- list all symbols, or ::nm -- give the symbol corresponding to if known ::walk -- list addresses of data structures of a given type; accepted types are listed by ::walkers (e.g., thread, proc) = -- the address of symbol formatted as (see ::formats) E.g., the address of the 'getpid' function can be printed in hexadecimal, whole or in part, as follows: > ::formats !grep hexadecimal B - hexadecimal int (1 byte) J - hexadecimal long long (8 bytes) K - hexadecimal uintptr_t (8 bytes) X - hexadecimal int (4 bytes) Z - write hexadecimal long long (8 bytes) x - hexadecimal short (2 bytes) > getpid=Z fffffffffba44168 > getpid=K fffffffffba44168 > getpid=J fffffffffba44168 > getpid=X fba44168 > getpid=x 4168 > getpid=B 68 Instead of printing a symbol's value (i.e., virtual address), you can apply disassembly: > getpid::dis getpid: pushq %rbp getpid+1: movq %rsp,%rbp getpid+4: subq $0x10,%rsp getpid+8: movq %gs:0x18,%rax getpid+0x11: movq 0x190(%rax),%r9 getpid+0x18: movq 0xb0(%r9),%r8 getpid+0x1f: movl 0x4(%r8),%eax getpid+0x23: movl %eax,-0x8(%rbp) getpid+0x26: testl $0x400,0xcc(%r9) getpid+0x31: jne +0x9 getpid+0x33: movl 0x34(%r9),%eax getpid+0x37: movl %eax,-0x4(%rbp) getpid+0x3a: jmp +0x2c getpid+0x3c: movq %gs:0x18,%rax getpid+0x45: movq 0x190(%rax),%r8 getpid+0x4c: movq 0x620(%r8),%r8 getpid+0x53: movq 0x150(%r8),%r8 getpid+0x5a: movq 0xb0(%r8),%r8 getpid+0x61: movl 0x4(%r8),%eax getpid+0x65: movl %eax,-0x4(%rbp) getpid+0x68: movq -0x8(%rbp),%rax getpid+0x6c: leave getpid+0x6d: ret (notice that the disassembly listing continues till the end of the function; this is because symbol info includes length when defined - for a contiguous function, the length of its compiled binary code). or print the contents of memory interpreted according to a C struct's layout: ::print -- print the contents of RAM at interpreted as E.g.: "p0::print proc_t" (same result for "p0=Z ; .::print proc_t") Try disassembling a symbol that is not a function, e.g., "p0::dis", and see what happens. Explain why. Actually, "p0::print" will work without even specifying proc_t, the type of p0, explicitly. This is because the kernel's symbol tables keep type information for its named data object entries. ::print enables sophisticated "manual" kernel walks. For example, walking the process table to an entry for a process, and then to the struct that contains its PID: > ::ps ! grep bash R 15565 2777 15565 15565 101 0x4a014000 ffffff059a6860a0 bash ^^^^^ <--- this the PID as reported by ::ps > ffffff059a6860a0::print proc_t !grep p_pidp p_pidp = 0xffffff0616e13b68 > 0xffffff0616e13b68::print 'struct pid' // OR > ffffff059a6860a0::print proc_t p_pidp | ::print 'struct pid' { pid_prinactive = 0 pid_pgorphaned = 0x1 pid_padding = 0 pid_prslot = 0x84 pid_id = 0x3ccd <<--------------------PID, same as 15565 pid_pglink = 0xffffff059a6860a0 pid_pgtail = 0xffffff059a6860a0 pid_link = 0 pid_ref = 0x5 } So ::ps reported the correct PID, which p0->p_pidp->pid_id holds. You can check this without leaving the debugger: > !printf "%d\n" 0x3ccd 15565 The same traversal can be done with > ffffff059a6860a0::print proc_t p_pidp | ::print 'struct pid' pid_id pid_id = 0x3ccd =================================================================== How the "mdb -k" magic works: MDB is a userland process. It cannot directly read kernel memory; no userland process can. How, then, is "mdb -k" able to examine the kernel memory and keep up-to-date knowledge of kernel symbols and memory objects? MDB interacts with the kernel by reading two special pseudo-files, through which the kernel exposes its internal data: /dev/kmem and /dev/ksyms (see 'man /dev/kmem' and 'man /dev/ksyms' for hints). In fact, we can check that "mdb -k" process has these files opened: > ::ps ! grep mdb R 12485 3192 12485 2780 0 0x4a004200 ffffff015b4f0080 mdb > ffffff015b4f0080::pfiles FD TYPE VNODE INFO 0 CHR ffffff015b88fa80 /dev/pts/1 1 CHR ffffff015b88fa80 /dev/pts/1 2 CHR ffffff015b88fa80 /dev/pts/1 3 PROC ffffff01710a4a80 /proc/12485/as (proc=ffffff015b4f0080) 4 CHR ffffff015a3dc200 /devices/pseudo/mm@0:kmem 5 CHR ffffff01710a3600 /devices/pseudo/mm@0:mem 6 CHR ffffff0154189d00 /devices/pseudo/ksyms@0:ksyms > !ls -l /dev/kmem /dev/ksyms lrwxrwxrwx 1 root root 27 2012-01-04 19:10 /dev/kmem -> ../devices/pseudo/mm@0:kmem lrwxrwxrwx 1 root root 31 2012-01-04 19:10 /dev/ksyms -> ../devices/pseudo/ksyms@0:ksyms Double-check: (from process id to its kernel proc_t descriptor) > 0t12485::pid2proc ffffff015b4f0080 (the pid2proc command can be found with > ::dcmds ! grep pid pid2proc - convert PID to proc_t address -- by now you get the idea) Note that /dev/ksyms appears to userland processes as an executable files, but there is neither code nor data in it. It only contains the symbols tables in the format suitable for ELF library functions. There is a bit of complication: readelf won't be able to read it: root@openindiana:/home/sergey# readelf -a /dev/ksyms readelf: Error: '/dev/ksyms' is not an ordinary file But we can save a copy of it as an ordinary file and then read it: cat /dev/ksyms > /tmp/ksyms ; readelf -a /tmp/ksyms (Do it! see the sections and segments in the resulting file. Compare the addresses of symbols in this file with those reported by ::nm inside MDB) =============================================================== The pseudo-file /dev/kmem presents the kernel's virtual memory space to (sufficiently privileged) userland processes. This pseudo-file can only be read at offsets that correspond to valid (mapped) kernel virtual addresses; attempts to dd it will fail, because the first pages (starting with the NULL address) of the kernel's address space are not mapped. When read at offsets that are mapped, it will provide the contents of kernel memory byte-for-byte, and this is how "mdb -k" walks the kernel. Note that using such large offsets requires that userland programs understand 64 bit addresses. dd is not capable of this, as it was written in 32 bit times. For the simplest example of a program that reads kernel memory through /dev/kmem, see readat.c (see examples/) > ffffff059a6860a0,4/X 0xffffff059a6860a0: 5afcbb00 ffffff01 54111af0 ffffff01 (running ./readat, compiled from readat.c with "gcc -Wall -o readat readat.c" from the shell:) > !./readat 0xffffff059a6860a0 00 bb fc 5a 01 ff ff ff f0 1a 11 54 01 ff ff ff Same bytes (observe home the endianness presents the results, try 'ffffff059a6860a0,0t16/B') ==============================================================