GNU/Linux系统可能同时运行数百个任务,但是系统中只有一张网卡、一块硬盘、一个键盘等。Linux内核负责分配这些有限的资源,控制任务对资源的访问。这就避免了两个任务不小心搞乱磁盘文件中的数据。

当你运行应用程序时,它会用到用户空间库(例如printffopen这样的函数)和系统空间库(例如writeopen这样的函数)。如果程序调用printf(或是脚本调用echo命令)格式化输出字符串,它调用的就是用户空间库函数printf。该函数接着会再调用系统空间库函数write。系统调用会确保一次只有一个任务能够访问特定的资源。

在理想情况下,所有的计算机程序各行其道,不出任何问题。在相对理想的情况下,你拥有源代码,程序在编译时加入了调试支持,即便出了故障,也能表现出一致性。

在现实情况下,你有时候不得不同没有源代码的程序打交道,这些程序还会出现间歇性故障。开发人员也爱莫能助,除非你能给他们一些工作数据。

Linuxstrace命令能够输出应用程序所用到的系统调用,这可以在没有源代码的情况下帮助我们理解程序的意图。

11.5.1 预备知识

strace是作为开发者软件包(Developer package)的一部分安装的,也可以单独进行安装:

  1. $ sudo apt-get install strace
  2. $ sudo yum install strace

11.5.2 实战演练

理解strace的一种方法就是编写一个简短的C程序,然后使用strace查看涉及的系统调用。

这个测试程序会分配内存,然后使用分配到的内存打印出一条信息,再释放内存,最后退出。

strace的输出显示了该程序所调用的系统函数:

  1. $ cattest.c
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. #include <string.h>
  5. main () {
  6. char *tmp;
  7. tmp=malloc(100);
  8. strcat(tmp, "testing");
  9. printf("TMP: %s\n", tmp);
  10. free(tmp);
  11. exit(0);
  12. }
  13. $ gcctest.c
  14. $ strace ./a.out
  15. execve("./a.out", ["./a.out"], [/* 51 vars */]) = 0
  16. brk(0) = 0x9fc000
  17. mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) =
  18. 0x7fc85c7f50002
  19. access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or
  20. directory)
  21. open("/etc/ld.so.cache", O_RDONLY) = 3
  22. fstat(3, {st_mode=S_IFREG|0644, st_size=95195, ...}) = 0
  23. mmap(NULL, 95195, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fc85c7dd000
  24. close(3) = 0
  25. open("/lib64/libc.so.6", O_RDONLY) = 3
  26. read(3,"\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0000\356\1\16;\0\0\0"...,832) = 832
  27. fstat(3, {st_mode=S_IFREG|0755, st_size=1928936, ...}) = 0
  28. mmap(0x3b0e000000, 3750184, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE,3, 0) = 0x3b0e000000
  29. mprotect(0x3b0e18a000, 2097152, PROT_NONE) = 0
  30. mmap(0x3b0e38a000, 24576, PROT_READ|PROT_WRITE,
  31. MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x18a000) = 0x3b0e38a000
  32. mmap(0x3b0e390000, 14632, PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x3b0e390000
  33. close(3) = 0
  34. mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) =
  35. 0x7fc85c7dc000
  36. mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) =
  37. 0x7fc85c7db000
  38. mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) =
  39. 0x7fc85c7da000
  40. arch_prctl(ARCH_SET_FS, 0x7fc85c7db700) = 0
  41. mprotect(0x3b0e38a000, 16384, PROT_READ) = 0
  42. mprotect(0x3b0de1f000, 4096, PROT_READ) = 0
  43. munmap(0x7fc85c7dd000, 95195) = 0
  44. brk(0) = 0x9fc000
  45. brk(0xa1d000) = 0xa1d000
  46. fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 11), ...}) = 0
  47. mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) =
  48. 0x7fc85c7f4000
  49. write(1, "TMP: testing\n", 13) = 13
  50. exit_group(0) = ?
  51. +++ exited with 0 +++

11.5.3 工作原理

第一行是应用程序的标准启动步骤。系统调用execve用于初始化新的可执行代码。brk调用可以返回当前的内存地址,mmap调用为动态链接库和状态信息分配了4096字节的内存。

访问ld.so.preload失败的原因在于ld.so.preload是一个用于预装载库代码的钩子。在大多数生产系统(production sysytem)中并不需要它。

ld.so.cache/etc/ld.so,conf.d在内存中的副本,其中包含了动态链接库的装载路径。这些内容会保存在内存中,以降低启动程序时的开销。

接下来出现的系统调用mmapmprotectarch_prctlmunmap继续载入库代码并将设备映射到内存中。

程序中的malloc调用引发了两次brk系统调用。结果是从堆中分配了100字节。

strcat是用户空间函数,不会引发系统调用。

printf也不会引发系统调用,它会将格式化过的字符串发送到stdout

fstatmmap系统调用载入并初始化stdout设备。这两个调用在程序中只出现了一次,用于生成stdout的输出。

write系统调用将字符串发往stdout

最后,exit_group系统调用负责退出程序、释放资源以及终止与进程相关的所有线程。

注意,并没有与释放内存操作相对应的brk系统调用。mallocfree函数是用于管理任务内存的用户空间函数。它们仅在程序总的内存占用情况发生变化时才会调用brk。如果程序分配了N字节的内存,这些内存会被添加到其可用内存中。当进行释放时,这部分内存会被标为不可用状态,但仍会被保留在程序的内存池中。下一次调用malloc时,就会从内存池中划分,直到耗尽为止。这时候才会再次调用brk从系统申请更多的内存。