Makefile.
For this introductory lab, you will implement a system call tracing program using the ptrace API.
Your program must be written in C unless you discuss alternate langauge choices with the instructor ahead of time.
The command to run a program with your system call tracer should look something like this:
$ ./trace ls -l
This command should launch the ls program under tracing with -l as a command line argument.
As ls makes system calls, they should be written to the terminal.
For every system call, you must report:
readptrace function has a mode that allows you to copy bytes from a given address in the tracee.You are welcome to use a different format, but a reasonable implementation could produce the following output when tracing the ls program:
$ ./trace ls
execve("/usr/bin/ls", ["ls"], 0x7fff05124c60) = 0
brk(NULL) = 0x55a42bc32000
mmap(NULL, 8192, 3, 34, -1, 0) = 0x7fac561e4000
access("/etc/ld.so.preload", 4) = -2
openat(-100, "/etc/ld.so.cache", 524288) = 3
[trace continues for many lines]
As you can see in the example above, the tracer prints strings and the NULL-terminated array of strings used in execve, but other values are printed as decimal or hexadecimal values.
You are welcome to write code to translate integer options to functions like mmap or openat into their #defined constants, but this is not a requirement for the lab.
You will need to turn in the code for your tracing program and a Makefile.
Running make with no arguments should compile your tracing program.
You are welcome to reuse Makefiles from your CSC 213 work or ask for help setting up a Makefile.
Linux has many system calls, but you do not need to interpret all of them.
Your tracing program should be able to trace and print any system calls issued by ls, grep, or nano on MathLAN.
I recommend starting with a system that prints system calls by number only, and then add special cases to convert system call numbers to names and print arguments gradually as you encounter system calls in these programs.
The example trace above was produced using the Linux program strace, which is available on MathLAN.
You can run a program with strace much like the trace program you are writing.
Keep in mind, strace supports many more features than you are required to implement.
You do not need to convert integer options into human-readable constants or add support for tracing programs that use fork to create sub-processes.
Make sure your C implementation uses good C practices. These include, but are not limited to:
You may implement your program entirely in one source file, or across multiple sources files.
ptraceYou will almost certainly need to use the ptrace function with the PTRACE_SYSCALL option.
This will run programs up until they start a system call, at which point you can examine the system call number and any arguments to the system call.
You can convert numbers to system call names using a table of system calls. Double-clicking a system call on the previously linked page will show you which registers hold each argument to the system call.
There are some good guides on how to use ptrace online, though some are a bit outdated.
The following example should be a good starting point for tracing system calls:
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <asm/unistd.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/user.h>
int main(int argc, char** argv) {
// Call fork to create a child process
pid_t child_pid = fork();
if(child_pid == -1) {
perror("fork failed");
exit(2);
}
// If this is the child, ask to be traced
if(child_pid == 0) {
if(ptrace(PTRACE_TRACEME, 0, NULL, NULL) == -1) {
perror("ptrace traceme failed");
exit(2);
}
// Stop the process so the tracer can catch it
raise(SIGSTOP);
// Do some work in the traced child process here. You'll eventually want to
// start the progam passed in the arguments to main.
if(execlp("ls", "ls", NULL)) {
perror("execlp failed");
exit(2);
}
} else {
// Wait for the child to stop
int status;
int result;
do {
result = waitpid(child_pid, &status, 0);
if(result != child_pid) {
perror("waitpid failed");
exit(2);
}
} while(!WIFSTOPPED(status));
// We are now attached to the child process
printf("Attached!\n");
// Now repeatedly resume and trace the program
bool running = true;
int last_signal = 0;
while(running) {
// Continue the process, delivering the last signal we received (if any)
if(ptrace(PTRACE_SYSCALL, child_pid, NULL, last_signal) == -1) {
perror("ptrace CONT failed");
exit(2);
}
// No signal to send yet
last_signal = 0;
// Wait for the child to stop again
if(waitpid(child_pid, &status, 0) != child_pid) {
perror("waitpid failed");
exit(2);
}
if(WIFEXITED(status)) {
printf("Child exited with status %d\n", WEXITSTATUS(status));
running = false;
} else if(WIFSIGNALED(status)) {
printf("Child terminated with signal %d\n", WTERMSIG(status));
running = false;
} else if(WIFSTOPPED(status)) {
// Get the signal delivered to the child
last_signal = WSTOPSIG(status);
// If the signal was a SIGTRAP, we stopped because of a system call
if(last_signal == SIGTRAP) {
// Read register state from the child process
struct user_regs_struct regs;
if(ptrace(PTRACE_GETREGS, child_pid, NULL, ®s)) {
perror("ptrace GETREGS failed");
exit(2);
}
// Get the system call number
size_t syscall_num = regs.orig_rax;
// Print the systam call number and register values
// The meanings of registers will depend on the system call.
// Refer to the table at https://filippo.io/linux-syscall-table/
printf("Program made system call %lu.\n", syscall_num);
printf(" %%rdi: 0x%llx\n", regs.rdi);
printf(" %%rsi: 0x%llx\n", regs.rsi);
printf(" %%rdx: 0x%llx\n", regs.rdx);
printf(" ...\n");
last_signal = 0;
}
}
}
return 0;
}
}
You should be able to run the code above and see it trace the execution of the ls command.
You’ll notice that ptrace stops the process immediately before and after each system call;
you only need to check if a system call is allowed before it executes, so you may need to change or rewrite the loop in the code above.
Remember that processes do not share memory.
For system calls that use pointer parameters, the pointers will refer to memory in the traced process, not the tracer.
It is always unsafe to dereference these pointers in the tracer process.
Instead, you will need to use ptrace to access memory in the traced process.