strace is a diagnostic and debugging tool on Linux that allows you to trace the system calls made by a program during its execution. System calls are the interface between user applications and the kernel, enabling the programs to request services such as file access, process management, and memory allocation from the kernel.
strace can be used to monitor and log the system calls a program makes, along with the signals it receives and the returned value from the system calls. This information can be very helpful in diagnosing issues, understanding the behavior of the program, and identifying performance bottlenecks.
To use strace , you can simply prefix the command you want to trace with the strace command. For example, if you want to trace the system calls made by ls command, you would run:
strace ls
strace will then display the system calls, their arguments, and return values in real-time as the program executes.
Here are some ways strace can help with system calls.
1-Debugging: by observing the system calls and their return values, you can identify issues such as file access errors, incorrect parameters, or memory allocation failures that may be causing your program to misbehaving or crash.
2-Performance analysis: strace can help you identify the performance bottlenecks by showing you which system calls are being executed frequently or take a long time to complete. This can help you optimize your code to minimize slow or unnecessary calls.
3-understanding program behavior: examining the system calls made by a program can provide insights into its internal working, making it easier to understand how it interacts with the underlying system.
4-security analysis: Monitoring the system calls made by an unknown program can help you identify potentially malicious behavior, such as unauthorized file access, network communication, … .
Overall, strace is a valuable tool for developers, system administrators, and security professionals, who need to understand the interactions between program and the Linux kernel through system calls.
Let’s dive into a practical example to demonstrate how you can effectively use strace on Linux.
#include <iostream>
#include <cstdio>
#include <sys/utsname.h>
#include <vector>
#include <string>
#include <stdexcept>
using namespace std;
string execute_command(const string& command)
{
string result;
vector<char> buffer(128);
FILE *fp=popen(command.c_str(),"r");
if(fp != nullptr)
{
try
{
while (fgets(buffer.data(),buffer.size(),fp)!=nullptr)
{
result+=buffer.data();
}
}
catch(const exception& e)
{
pclose(fp);
std::cerr << e.what() << '\n';
throw;
}
pclose(fp);
}
else
{
throw runtime_error("Failed to execute command: "+command);
}
return result;
}
int main()
{
struct utsname sysinfo;
if(uname(&sysinfo)==0)
{
cout<<"system: "<<sysinfo.sysname<<endl;
cout<<"Node name: "<<sysinfo.nodename<<endl;
cout<<"Release: "<<sysinfo.release<<endl;
cout<<"Version: "<<sysinfo.version<<endl;
cout<<"Machine: "<<sysinfo.machine<<endl;
}
else
{
cerr<<"Failed to get system information ."<<endl;
}
try
{
string date_output=execute_command("date");
cout<<"current date: "<<date_output<<endl;
}
catch(const exception& e)
{
cerr<<e.what()<<endl;
return 1;
}
return 0;
}
The above code is for testing and uses the popen, pclose, fgets, and uname system calls.
Get Mohammad Abdoli’s stories in your inbox
Join Medium for free to get updates from this writer.Subscribe
Now to trace the system calls of the program we use the below command.
strace ./main
strace will output the system calls made by the program as it executes. Here is the output:
execve("./main", ["./main"], 0x7ffc640ec270 /* 52 vars */) = 0
brk(NULL) = 0x5583406a5000
arch_prctl(0x3001 /* ARCH_??? */, 0x7ffc2dcbe220) = -1 EINVAL (Invalid argument)
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=133546, ...}) = 0
mmap(NULL, 133546, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f9c62745000
close(3) = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libstdc++.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0`\341\t\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0644, st_size=1956992, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f9c62743000
mmap(NULL, 1972224, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f9c62561000
mprotect(0x7f9c625f7000, 1290240, PROT_NONE) = 0
mmap(0x7f9c625f7000, 987136, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x96000) = 0x7f9c625f7000
mmap(0x7f9c626e8000, 299008, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x187000) = 0x7f9c626e8000
mmap(0x7f9c62732000, 57344, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1d0000) = 0x7f9c62732000
mmap(0x7f9c62740000, 10240, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f9c62740000
close(3) = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libgcc_s.so.1", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\3405\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0644, st_size=104984, ...}) = 0
mmap(NULL, 107592, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f9c62546000
mmap(0x7f9c62549000, 73728, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x3000) = 0x7f9c62549000
mmap(0x7f9c6255b000, 16384, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x15000) = 0x7f9c6255b000
mmap(0x7f9c6255f000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x18000) = 0x7f9c6255f000
close(3) = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\300A\2\0\0\0\0\0"..., 832) = 832
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
pread64(3, "\4\0\0\0\20\0\0\0\5\0\0\0GNU\0\2\0\0\300\4\0\0\0\3\0\0\0\0\0\0\0", 32, 848) = 32
pread64(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\0\30x\346\264ur\f|Q\226\236i\253-'o"..., 68, 880) = 68
fstat(3, {st_mode=S_IFREG|0755, st_size=2029592, ...}) = 0
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
pread64(3, "\4\0\0\0\20\0\0\0\5\0\0\0GNU\0\2\0\0\300\4\0\0\0\3\0\0\0\0\0\0\0", 32, 848) = 32
pread64(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\0\30x\346\264ur\f|Q\226\236i\253-'o"..., 68, 880) = 68
mmap(NULL, 2037344, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f9c62354000
mmap(0x7f9c62376000, 1540096, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x22000) = 0x7f9c62376000
mmap(0x7f9c624ee000, 319488, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x19a000) = 0x7f9c624ee000
mmap(0x7f9c6253c000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1e7000) = 0x7f9c6253c000
mmap(0x7f9c62542000, 13920, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f9c62542000
close(3) = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libm.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\300\323\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0644, st_size=1369384, ...}) = 0
mmap(NULL, 1368336, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f9c62205000
mmap(0x7f9c62212000, 684032, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0xd000) = 0x7f9c62212000
mmap(0x7f9c622b9000, 626688, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0xb4000) = 0x7f9c622b9000
mmap(0x7f9c62352000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x14c000) = 0x7f9c62352000
close(3) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f9c62203000
arch_prctl(ARCH_SET_FS, 0x7f9c62204100) = 0
mprotect(0x7f9c6253c000, 16384, PROT_READ) = 0
mprotect(0x7f9c62352000, 4096, PROT_READ) = 0
mprotect(0x7f9c6255f000, 4096, PROT_READ) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f9c62201000
mprotect(0x7f9c62732000, 45056, PROT_READ) = 0
mprotect(0x55833f304000, 4096, PROT_READ) = 0
mprotect(0x7f9c62793000, 4096, PROT_READ) = 0
munmap(0x7f9c62745000, 133546) = 0
brk(NULL) = 0x5583406a5000
brk(0x5583406c6000) = 0x5583406c6000
uname({sysname="Linux", nodename="mominux-a485", ...}) = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x1), ...}) = 0
write(1, "system: Linux\n", 19system: Linux
) = 19
write(1, "Node name: mominux-a485\n", 26Node name: mominux-a485
) = 26
write(1, "Release: 5.15.0-69-generic\n", 31Release: 5.15.0-69-generic
) = 31
write(1, "Version: #76~20.04.1-Ubuntu "..., 65Version: #76~20.04.1-Ubuntu SMP Mon Mar 20 15:54:19 UTC 2023
) = 65
write(1, "Machine: x86_64\n", 20Machine: x86_64
) = 20
pipe2([3, 4], O_CLOEXEC) = 0
prlimit64(0, RLIMIT_NOFILE, NULL, {rlim_cur=1024, rlim_max=1024*1024}) = 0
prlimit64(0, RLIMIT_NOFILE, NULL, {rlim_cur=1024, rlim_max=1024*1024}) = 0
mmap(NULL, 36864, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0x7f9c6275d000
rt_sigprocmask(SIG_BLOCK, ~[], [], 8) = 0
clone(child_stack=0x7f9c62765ff0, flags=CLONE_VM|CLONE_VFORK|SIGCHLD) = 9209
munmap(0x7f9c6275d000, 36864) = 0
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
close(4) = 0
fcntl(3, F_SETFD, 0) = 0
fstat(3, {st_mode=S_IFIFO|0600, st_size=0, ...}) = 0
read(3, "Fri 21 Apr 2023 07:32:30 PM CEST"..., 4096) = 33
read(3, "", 4096) = 0
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=9209, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---
close(3) = 0
wait4(9209, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 9209
write(1, "current date: Fri 21 Apr 2023 07"..., 47current date: Fri 21 Apr 2023 07:32:30 PM CEST
) = 47
write(1, "\n", 1
) = 1
exit_group(0) = ?
+++ exited with 0 +++
The output shows the calls made by the program to the OS, as well as the result of those calls. Here is the categorized analysis of the output.
1- process control and memory management:
execve: Execute a new program, replacing the current process image.
brk: changes the location of the program break, which defines the end of the process`s data segment.
arch_prctl: sets architecure-specific thread state.
mmap: maps memory regions, either to file or anonymous memory.
mprotect: modifies protection of memory pages.
munmap: unmaps memory regions.
clone: creates a new child process by duplicate the calling process.
wait4: waits for a child process to change state.
exit_group: exits all threads in a process.
2- File I/O operations:
access: Checks if the calling process can access the specified file.
openat: Opens a file relative to a directory file descriptor.
read: Reads data from a file descriptor.
pread64: Reads data from a file descriptor at a specified offset.
fstat: Retrieves information about an open file.
close: Closes a file descriptor.
write: Writes data to a file descriptor.
fcntl: Performs operations on a file descriptor.
3- Signal handling:
rt_sigprocmask: Examines or changes a process’s signal mask.
SIGCHLD: A signal sent to a parent process when a child process terminates, stops, or continues.
4- Resource management:
pipe2: Creates a unidirectional data channel that can be used for interprocess communication.
prlimit64: Retrieves and/or sets resource limits.
5- System information:
uname: Retrieves system information, such as system name, node name, release, version, and machine type.
In conclusion, strace is a powerful and versatile tool for system programming, as it offers insights into the interactions between user applications and the Linux kernel through system calls. By using strace, developers can effectively debug their code, identify performance bottlenecks, understand program behavior, and analyze security aspects. Through the practical example provided in this article, it is evident how strace can be used to trace the system calls made by a program during its execution, thus making system programming and debugging significantly easier. Whether you are a developer, system administrator, or security professional, mastering strace will undoubtedly prove invaluable in your work with Linux-based systems.
References:
- strace pdf
- Zoppetti, G. M. (2017). System Programming with C and Unix. Pearson.
- Strace — Linux Man Page
- Linux System Call Table