12 Interrupts

4 minute read

Published:

Debugger breakpoint

How is it implemented? Tracer fork()s, and the child program becomes the tracee, but before that, makes a syscall ptrace(), so the parent process can control this.

Instruction int3, OS stops the program, and sends info to the tracer of the program

Processor modes

  • User mode (mode bit = 1): can’t do privileged operations
  • Kernel mode (mode bit = 0): can performed privileged operations and has full hardware access Allows user program to “trap” into kernel and “return from trap”

Interrupt

Stop the CPU from executing the current instructions and makes it jump to a predetermined piece of OS kernel code

  • If a user process does while(1), OS must have ways to interrupt this,
  • Time interrupt handler sees if a process is running too long. If so, it will put it to sleep and run others.
  1. Hardware interrupt
    • Asynchronous (can arrive at any time)
    • Timer interrupt
    • Network packet arrival, key press, …
  2. Exceptions & Faults
    • Synchronous: result of executing some instruction
    • E. division by 0, segmentation fault, page fault
  3. Software interrupt
    • Synchronous, deliberately raised by software (int <code>) in x86
    • System call
    • Debugger breakpoints (int3, single byte)

CPU cycle

while (1) {
	if (HWint or exception) {
		n = int/exception type    // exception flag
		call interrupt handler n
	}

	fetch next instrction
	if (instruction == int n)
		call interrupt handler n
	else
		run instruction
}

Interrupts are not userspace signals

  • Signals: enables communication between processes and OS
  • Interrupts: enables communication between hardware and OS
    • but some interrupts can generate userspace signals…
      • divide by zero exception: SIGFPE
      • illegal memory access: SIGSEGV
      • software breakpoint: SIGTRAP

Linux System Call

User program invokes read():

  1. libc system call wrapper invokes software interrupt 0x80 (system call)
    • Places syscall number __NR_read into %eax register
       movl __NR_read, %eax
       int 0x80                # syscall, enter kernel mode
       ret
      
    • (also puts other parameters in the register)
  2. Trap into kernel mode, look up 0x80 in Interrupt Descriptor Table (IDT)
  3. Jump to interrupt 0x80’s handler: system_call()
  4. system_call() looks up __NR_read in sys_call_table
  5. Unpack registers, call __NR_read’s handler: sys_read()
  6. Perform read()’s real work in kernel’s sys_read()
    • file entry management, I/O requests, copying data, etc.

Breakpoints

Stop execution at arbitrary points, E. using int3

  • Debugger replace the byte at addr with int3 and restore the byte later
  • When int3 reached, trigger interrupt, OS takes over, notify debugger (SIGTRAP)
  • Debugger intercepts SIGTRAP and regains control Only works for breakpoints at an instruction
  • E. can’t stop if a variable has changed
  • CPU offers hardware breakpoints (watchpoints)

ldb demo

We set linux to store address at 400000, but instruction pointer is set to be a huge address: hasn’t entered main() yet.

  • Starts with some library routine that’s going to call main()

Workflow

ldb.c

  • personality(ADDR_NO_RANDOMIZE): objdump content will be exactly the actual memory

Tracee will stop each time any signal is delivered, even if signals are ignored

  • The tracer will be notified at the next call to waitpid()
  • waitpid() will return a status value, telling why child is stopped (any signals received)
    • Whenever kernel wants to stop the child (E. at the beginning of execve(), kernel tells the debugger SIGTRAP)
    • Child didn’t receive SIGTRAP, just telling the debugger that it has the control
  • Debugger can inspect or modify the tracee
  • Tracer can proceed, delivering the signal or not sumpause
  • Call c, waitpid() until it’s completed
  • When SIGINT sent to debugger, ldb_read_signal() reads signals and stops the child
  • Now can do things to the chil
  • If c again, the signal will be delivered to the child

Breakpoint

b <insr>
x <instr>    # modified to cc
c            # break at <instr>
s            # put back the instruction, but then put `cc` back
objdump ./sum

ldb ./sum
(ldb) b 40114e     # break at the next 40114f
(ldb) c            # continue
(ldb@40114f*) t
# Gives a backtrace
# 0x40114f in sum:401136 (starting address of the func)
#  called from 0x... in sum_array:...
#  called from 0x... in main:...

Unroll the stack and backtraces, stop until hit main

  • The return address of the stack is printed
  • That’s why the address printed is not the call address It also figures out what function makes the call: sum:401136
  • Need to read the executable file (as objdump), mmap() it
  • How: ELF