├── README.pdf ├── task1 ├── fantasy.c └── reverse.c ├── task2 ├── interesting.s ├── step.c └── breakpoint.c └── README.md /README.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jinningli/system2018-project1/HEAD/README.pdf -------------------------------------------------------------------------------- /task1/fantasy.c: -------------------------------------------------------------------------------- 1 | #include "stdio.h" 2 | 3 | int main() 4 | { 5 | printf("Oh, Fantasy!\n"); 6 | return 0; 7 | } 8 | -------------------------------------------------------------------------------- /task2/interesting.s: -------------------------------------------------------------------------------- 1 | .section .data 2 | 3 | s1:.string "Oh, \n" 4 | s2:.string "interesting!\n" 5 | 6 | .section .text 7 | .globl _start 8 | 9 | _start: 10 | 11 | #print oh 12 | movl $4, %eax 13 | movl $1, %ebx 14 | movl $s1, %ecx 15 | movl $5, %edx 16 | int $0x80 17 | 18 | #print interesting 19 | movl $4, %eax 20 | movl $1, %ebx 21 | movl $s2, %ecx 22 | movl $13, %edx 23 | int $0x80 24 | 25 | movl $1, %eax 26 | int $0x80 27 | -------------------------------------------------------------------------------- /task2/step.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | /** 15 | * 使用PTRACE_SINGLESTEP对汇编代码进行单步调试 16 | * */ 17 | int main(int argc, char** argv) { 18 | if (argc < 2) { 19 | perror("set a progarm to exec"); 20 | return -1; 21 | } 22 | pid_t child_pid = fork(); 23 | if (child_pid == 0) { 24 | //子进程执行 25 | /** 26 | * TODO 子程序执行啥? 27 | **/ 28 | } else if (child_pid > 0) { 29 | //父进程执行 30 | int cnt = 0; 31 | int status; 32 | while (1) { 33 | //等待子进程信号 34 | wait(&status); 35 | if (WIFEXITED(status)) //子进程发送退出信号,退出循环 36 | break; 37 | cnt++; 38 | //调用ptrace从子进程取数据 39 | struct user_regs_struct regs; 40 | /** 41 | * TODO 1.使用 PTRACE_GETREGS 获取所有寄存器值存在 ®s 里 42 | * TODO 2.使用 PTRACE_PEEKTEXT 获取当前指令内容 存在instr里 注意读取的是一个字,64位 43 | * 虽然X86-64是CISC,指令长度不一定是64位,总之我们就取64位的内存出来看看嘛 44 | * X86-64中,RIP寄存器用于指向当前执行的指令位置 45 | * 可以通过regs.rip来访问 RIP寄存器的值 46 | **/ 47 | printf("[%u] RIP = 0x%016x, Instruction = 0x%016x\n", cnt, 48 | regs.rip, instr); 49 | printf("EAX: 0x%08x ", ptrace(PTRACE_PEEKUSER, child_pid, 8 * RAX, NULL)); 50 | printf("EBX: 0x%08x\n", ptrace(PTRACE_PEEKUSER, child_pid, 8 * RBX, NULL)); 51 | printf("ECX: 0x%08x ", ptrace(PTRACE_PEEKUSER, child_pid, 8 * RCX, NULL)); 52 | printf("EDX: 0x%08x\n", ptrace(PTRACE_PEEKUSER, child_pid, 8 * RDX, NULL)); 53 | printf("Press any key to continue...\n"); 54 | getchar(); 55 | /** 56 | * TODO 使用 PTRACE_SINGLESTEP 来重新启动子进程,执行下一条指令后暂停。 57 | **/ 58 | } 59 | } else { 60 | perror("fork error"); 61 | return -1; 62 | } 63 | 64 | return 0; 65 | } 66 | -------------------------------------------------------------------------------- /task1/reverse.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | const int long_size = sizeof(long);//字长 若本机是64位,则字长应为8字节long 11 | 12 | /** 13 | * TODO 反转str指针指向的字符串 14 | **/ 15 | void reverse(char *str){ 16 | } 17 | 18 | /** 19 | * TODO 从子进程读数据, 也就是从首地址addr中读取len个字节,并写到str中。不要忘了加'\0' 20 | * 使用ptrace的 PTRACE_PEEKDATA 来读,需要注意的是由于64位机器的字长是8byte, 21 | * 所以ptrace每次读取的长度都是8byte。 22 | * 你可能会用到:memcpy函数 23 | **/ 24 | void getdata(pid_t child, long addr, char *str, int len){ 25 | } 26 | 27 | /** 28 | * TODO 往子进程写数据 从str中得到需要写的长度为len的字符串,写到地址addr中去。 29 | * 使用 ptrace 的 PTRACE_POKEDATA 来写,需要注意的是由于64位机器的字长是8byte。 30 | * */ 31 | void putdata(pid_t child, long addr, char *str, int len){ 32 | 33 | } 34 | 35 | int main(){ 36 | pid_t child; 37 | child = fork(); 38 | if (child < 0) { 39 | printf("fork error"); 40 | } else if(child == 0) { 41 | //子进程执行 42 | ptrace(PTRACE_TRACEME, 0, NULL, NULL); 43 | execl("fantasy", "fantasy", NULL); 44 | } else { 45 | //父进程执行 46 | long orig_rax; 47 | long params[3]; 48 | int status; 49 | char *str, *laddr; 50 | int toggle = 0; 51 | while(1) { 52 | //等待子进程信号 53 | wait(&status); 54 | if(WIFEXITED(status)) //遇到子进程退出信号,退出循环 55 | break; 56 | // 使用 PTRACE_PEEKUSER 来获取系统调用号。 57 | orig_rax = ptrace(PTRACE_PEEKUSER, child, 8 * ORIG_RAX, NULL); 58 | if(orig_rax == SYS_write) { 59 | if(toggle == 0) { 60 | toggle = 1; 61 | /** 62 | * TODO 使用 PTRACE_PEEKUSER 来获取 RDI, RSI, RDX寄存器的值, 63 | * 分别存到params[0], params[1], params[2]中。 64 | * 需要注意的是,RAX的宏定义是 ORIG_RAX, 65 | * 而 RDI RSI RDX 的宏定义为 RDI RSI RDX 66 | **/ 67 | str = (char *)calloc((params[2]+1), sizeof(char)); 68 | getdata(child, params[1], str, 69 | params[2]); 70 | reverse(str); 71 | putdata(child, params[1], str, 72 | params[2]); 73 | } else { 74 | toggle = 0; 75 | } 76 | } 77 | /** 78 | * TODO 使用 PTRACE_SYSCALL 来让子进程进行系统调用。 79 | **/ 80 | } 81 | } 82 | return 0; 83 | } 84 | -------------------------------------------------------------------------------- /task2/breakpoint.c: -------------------------------------------------------------------------------- 1 | /* Code sample: manual setting of a breakpoint, using ptrace 2 | ** 3 | ** Eli Bendersky (http://eli.thegreenplace.net) 4 | ** This code is in the public domain. 5 | */ 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | 21 | extern char* strsignal(int); 22 | 23 | /* Run a target process in tracing mode by exec()-ing the given program name. 24 | */ 25 | void run_target(const char* programname) 26 | { 27 | printf("target started. will run '%s'\n", programname); 28 | 29 | /* Allow tracing of this process */ 30 | if (ptrace(PTRACE_TRACEME, 0, 0, 0) < 0) { 31 | perror("ptrace"); 32 | return; 33 | } 34 | 35 | /* Replace this process's image with the given program */ 36 | execl(programname, programname, 0); 37 | } 38 | void run_debugger(pid_t child_pid, unsigned long instr_addr) 39 | { 40 | int wait_status; 41 | struct user_regs_struct regs; 42 | 43 | /* 等待子进程信号 */ 44 | wait(&wait_status); 45 | 46 | /* 取子进程寄存器 */ 47 | ptrace(PTRACE_GETREGS, child_pid, 0, ®s); 48 | /* eip是子进程执行的指令地址 */ 49 | printf("Child started. EIP = 0x%08x\n", regs.rip); 50 | 51 | /* 读取相应指令地址处对应的指令*/ 52 | unsigned long addr = instr_addr; 53 | unsigned long data = ptrace(PTRACE_PEEKTEXT, child_pid, (void*)addr, 0); 54 | printf("Original data at 0x%08x: 0x%08x\n", addr, data); 55 | 56 | /* 把'int 3'替换进 0x804808a处 对应的指令*/ 57 | unsigned data_with_trap = (data & 0xFFFFFF00) | 0xCC; 58 | ptrace(PTRACE_POKETEXT, child_pid, (void*)addr, (void*)data_with_trap); 59 | 60 | /* 再读 地址0x804808a处 对应的指令*/ 61 | unsigned readback_data = ptrace(PTRACE_PEEKTEXT, child_pid, (void*)addr, 0); 62 | printf("After trap, data at 0x%08x: 0x%08x\n", addr, readback_data); 63 | 64 | /* 让子进程运行直到遇到断点 int 3*/ 65 | ptrace(PTRACE_CONT, child_pid, 0, 0); 66 | 67 | wait(&wait_status); 68 | if (WIFSTOPPED(wait_status)) { 69 | printf("Child got a signal: %s\n", strsignal(WSTOPSIG(wait_status))); 70 | } else { 71 | perror("wait"); 72 | return; 73 | } 74 | 75 | /* 看看子进程目前执行到哪了 */ 76 | ptrace(PTRACE_GETREGS, child_pid, 0, ®s); 77 | printf("Child stopped at EIP = 0x%08x\n", regs.rip - 1); 78 | /*等 用户响应*/ 79 | getchar(); 80 | 81 | /* 移除int 3,把原先的指令写回去*/ 82 | ptrace(PTRACE_POKETEXT, child_pid, (void*)addr, (void*)data); 83 | 84 | /* 指令地址 -1*/ 85 | regs.rip -= 1; 86 | ptrace(PTRACE_SETREGS, child_pid, 0, ®s); 87 | 88 | ptrace(PTRACE_CONT, child_pid, 0, 0); 89 | 90 | wait(&wait_status); 91 | 92 | if (WIFEXITED(wait_status)) { 93 | printf("Child exited\n"); 94 | } 95 | else { 96 | printf("Error: Unexpected signal\n"); 97 | } 98 | } 99 | 100 | 101 | int main(int argc, char** argv) 102 | { 103 | pid_t child_pid; 104 | 105 | if (argc < 3) { 106 | fprintf(stderr, "Expected: child_name Instr_addr\n"); 107 | return -1; 108 | } 109 | 110 | unsigned long ins_addr = 0; 111 | sscanf(argv[2], "%x", &ins_addr); 112 | 113 | child_pid = fork(); 114 | if (child_pid == 0) 115 | //子进程 116 | run_target(argv[1]); 117 | else if (child_pid > 0) 118 | // 父进程 119 | run_debugger(child_pid, ins_addr); 120 | else { 121 | perror("fork"); 122 | return -1; 123 | } 124 | 125 | return 0; 126 | } 127 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #
Project 1: Fantasy Ptrace
2 | ## 概述 3 | ptrace是process和trace的简写,直译为进程跟踪。它提供了一种使父进程得以监视和控制其子进程的方式,它还能够改变子进程中的寄存器和内核映像,因而可以实现系统调用的跟踪和断点调试。 4 | ## Deadline 5 | #### 3.28日下午18:00 6 | 7 | ## 另外 8 | 可以顺便在右上角面帮咱点个Star哦~ 9 | 10 | ## 使用方法 11 | ### 64位 Ubuntu 12 | 妥。可以直接运行 13 | ### 64位 MAC OSX 14 | 不妥。解决方案: 15 | #### 1. 虚拟机 16 | - Parallels 软件 葡萄下载: https://pt.sjtu.edu.cn/details.php?id=140821 17 | - 安装好后在软件中安装一个Ubuntu 16.04,点击就装 18 | #### 2. 悄悄使用本可爱的助教实验室的服务器 19 | 使用ssh链接到服务器,密码是```system2018``` 20 | 21 | **[注意]** 22 | 23 | 禁止查看和编辑服务器上别人的代码 24 | 25 | 跑完后记得清理掉自己的代码哦`rm -r ~/system/Yourname` 26 | 27 | 密码不要外传 28 | 29 | 不要运行project1以外的程序 30 | ``` 31 | ssh jinning@anl.sjtu.edu.cn 32 | mkdir ~/system/Yourname 33 | ``` 34 | 然后`exit`回到本机 35 | 传送你的文件到服务器上的文件夹 36 | ``` 37 | scp -r YourFilePath jinning@anl.sjtu.edu.cn:~/system/Yourname 38 | ``` 39 | 然后连接到服务器上开始玩耍 40 | ``` 41 | ssh jinning@anl.sjtu.edu.cn 42 | cd ~/system/Yourname 43 | ``` 44 | ### 64位 Windows 45 | 不妥。解决方案: 46 | #### 1. 虚拟机 47 | - 自己找找看安装包哦 paralells 或 VMWare 48 | #### 2. 悄悄使用本可爱的助教实验室的服务器 49 | - 想法子用一些windows的软件ssh连接上去玩耍,和 上面的64位 MAC OSX 2. 步骤一样。 50 | 例如可以使用putty: https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html 51 | ``` 52 | putty -ssh jinning@anl.sjtu.edu.cn 53 | mkdir ~/system/Yourname 54 | ``` 55 | 以及 56 | ``` 57 | pscp -r YourFilePath jinning@anl.sjtu.edu.cn:~/system/Yourname 58 | ``` 59 | 60 | ## 32位机器 61 | #### 不妥。请想办法使用本可爱的助教实验室的服务器,或者把代码改写成32位机子能运行的代码。 62 | 63 | ## Task 1 输出反转 64 | ### 准备知识 65 | ### 进程 66 | 程序是一个静态的概念,它只是一些预先编译好的指令和数据集合的一个文件;而进程是一个动态的概念,它是程序运行时的一个过程。 67 | 68 | #### 创建进程 69 | ``` 70 | #include 71 | pid_t fork(void); 72 | 73 | // 返回值:子进程返回0,父进程返回子进程ID,出错返回-1 74 | ``` 75 | fork函数被调用一次,却能够返回两次,它可能有三种不同的返回值: 76 | - 在父进程中,fork返回新创建子进程的进程ID; 77 | - 在子进程中,fork返回0; 78 | - 如果出现错误,fork返回-1; 79 | 80 | 在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。可以通过fork返回的值来判断当前进程是子进程还是父进程。 81 | 82 | ### 系统调用 83 | 在现代的操作系统里,程序运行的时候,本身是没有权利访问多少系统资源的。由于系统有限的资源有可能被多个不同的应用程序同时访问,因此,如果不加以保护,那么各个应用程序难免产生冲突。所以现代操作系统都将可能产生冲突的系统资源给保护起来,阻止应用程序直接访问。这些资源包括文件、网络、IO、各种设备等。 84 | 每个操作系统都会提供一套接口,来封装对系统资源的调用,这套接口就是系统调用。 85 | 86 | 操作系统把进程空间分为了用户空间和内核空间,系统调用是运行在内核空间的,而应用程序基本都是运行在用户空间的。应用程序想要访问系统资源,就必须通过系统调用。用户空间的应用程序要想调用内核空间的系统调用,就需要从用户空间切换到内核空间,这一般是通过中断来实现的。什么是中断呢?中断是一个硬件或者软件发出的请求,要求CPU暂停当前的工作转手去处理更加重要的事情。中断一般具有两个属性,中断号和中断处理程序。在内核中,有一个叫做中断向量表的数组来存放中断号和中断处理程序。当中断到来的时候,CPU会根据中断号找到对应的中断处理程序,并调用它。中断处理程序执行完成后,CPU会继续执行之前的代码。 87 | 88 | 通常意义上,中断有两种类型,一种称为硬件中断,这种中断来自于硬件的异常或其他事件的发生;另一种称为软件中断,软件中断通常是一条指令(i386下是int),带有一个参数记录中断号。linux系统使用`int 0x80`来触发所有的系统调用,和中断一样,系统调用带有一个系统调用号,这个系统调用就像身份标识一样来表明是哪一个系统调用。32位下,这个系统调用号会放在eax寄存器中。64位下,这个系统调用号会放在rax寄存器中。 89 | 90 | 系统调用号表:http://blog.csdn.net/sinat_26227857/article/details/44244433 91 | 92 | 32位下如果系统调用有一个参数,那么参数通过ebx寄存器传入,x86下linux支持的系统调用参数至多有6个,分别使用6个寄存器来传递,它们分别是ebx、ecx、edx、esi、edi和ebp。64位下略有不同。 93 | 94 | 例如对于`write`调用,`ebx(64位为rdi)`存放的是fd; `ecx(64位为rsi)`存放的是字符串数据的地址; `edx(64位为rdx)`存放的是字符串长度。 95 | 96 | 触发系统调用后,CPU首先需要切换堆栈,当程序的当前栈从用户态切换到内核态后,会找到系统调用号对应的调用函数,它们都是以"sys_"开头的,当执行完调用函数后,返回值会存放在eax(rax)寄存器返回到用户态。 97 | 98 | 99 | ### 信号 100 | 信号是在软件层次上对中断机制的一种模拟,它是一种进程间异步通信的机制。 101 | 102 | 一个进程要发信号给另一个进程,可以使用这些函数:kill()、raise()、 sigqueue()、alarm()、setitimer()以及abort()。它其实是通过系统调用把信号先发给内核。当另一个进程从内核态回用户态的时候,它会先去找一下有没有发给自己的信号,如果有,就处理掉。 103 | 104 | 进程可以通过三种方式来响应一个信号: 105 | - 忽略信号,即对信号不做任何处理; 106 | - 捕捉信号。通过signal()定义信号处理函数,当信号发生时,执行相应的处理函数; 107 | - 执行缺省操作,Linux对每种信号都规定了默认操作。 108 | 109 | ### ptrace函数 110 | 函数原型如下: 111 | ``` 112 | #include 113 | long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data); 114 | ``` 115 | ptrace有四个参数: 116 | - enum \_\_ptrace\_request request:指示了ptrace要执行的命令。 117 | - pid\_t pid: 指示ptrace要跟踪的进程。 118 | - void \*addr: 指示要监控的内存地址。 119 | - void \*data: 存放读取出的或者要写入的数据。 120 | request参数决定了ptrace的具体功能: 121 | 122 | 1.PTRACE_TRACEME 123 | `ptrace(PTRACE_TRACEME,0 ,0 ,0)` 124 | 描述:子进程使用,使得本进程被其父进程所跟踪。 125 | 126 | 2.PTRACE\_PEEKTEXT, PTRACE\_PEEKDATA 127 | `ptrace(PTRACE_PEEKDATA, pid, addr, data)` 128 | 描述:从内存地址中读取一个字,数据由函数返回,pid表示被跟踪的子进程,内存地址由addr给出,data参数被忽略。 129 | 130 | 3.PTRACE\_POKETEXT, PTRACE\_POKEDATA 131 | `ptrace(PTRACE_POKEDATA, pid, addr, data)` 132 | 描述:往内存地址中写入一个字。pid表示被跟踪的子进程,内存地址由addr给出,data为所要被写入的数据。 133 | 134 | 4.PTRACE_PEEKUSER 135 | `ptrace(PTRACE_PEEKUSER, pid, addr, data)` 136 | 描述:从USER区域中读取一个字节,pid表示被跟踪的子进程,addr表示读取数据在USER区域的偏移量,返回值为函数返回值,data参数被忽略。 137 | 138 | 5.PTRACE_POKEUSER 139 | `ptrace(PTRACE_POKEUSER, pid, addr, data)` 140 | 描述:往USER区域中写入一个字节,pid表示被跟踪的子进程,USER区域地址由addr给出,data为需写入的数据。 141 | 142 | 6.PTRACE_CONT 143 | `ptrace(PTRACE_CONT, pid, 0, signal)` 144 | 描述:继续执行。pid表示被跟踪的子进程,signal为0则忽略引起调试进程中止的信号,若不为0则继续处理信号signal。 145 | 146 | 7.PTRACE_SYSCALL 147 | `ptrace(PTRACE_SYSCALL, pid, 0, signal)` 148 | 描述:继续执行。pid表示被跟踪的子进程,signal为0则忽略引起调试进程中止的信号,若不为0则继续处理信号signal。与PTRACE_CONT不同的是进行系统调用跟踪。在被跟踪进程继续运行直到调用系统调用开始或结束时,被跟踪进程被中止,并通知父进程。 149 | 150 | 8.PTRACE_KILL 151 | `ptrace(PTRACE_KILL,pid)` 152 | 描述:杀掉子进程,使它退出。pid表示被跟踪的子进程。 153 | 154 | 9.PTRACE_SINGLESTEP 155 | `ptrace(PTRACE_SINGLESTEP, pid, 0, signal)` 156 | 描述:设置单步执行标志,单步执行一条指令。pid表示被跟踪的子进程。signal为0则忽略引起调试进程中止的信号,若不为0则继续处理信号signal。当被跟踪进程单步执行完一个指令后,被跟踪进程被中止,并通知父进程。 157 | 158 | 10.PTRACE_ATTACH 159 | `ptrace(PTRACE_ATTACH, pid)` 160 | 描述:跟踪指定pid 进程。pid表示被跟踪进程。被跟踪进程将成为当前进程的子进程,并进入中止状态。 161 | 162 | 11.PTRACE_DETACH 163 | `ptrace(PTRACE_DETACH, pid)` 164 | 描述:结束跟踪。 pid表示被跟踪的子进程。结束跟踪后被跟踪进程将继续执行。 165 | 166 | 12.PTRACE_GETREGS 167 | `ptrace(PTRACE_GETREGS, pid, 0, data)` 168 | 描述:读取寄存器值,pid表示被跟踪的子进程,data为用户变量地址用于返回读到的数据。 169 | 170 | 13.PTRACE_SETREGS 171 | `ptrace(PTRACE_SETREGS, pid, 0, data)` 172 | 描述:设置寄存器值,pid表示被跟踪的子进程,data为用户数据地址。 173 | 174 | >更多详细的信息可以参考linux下man手册: 175 | http://linux.die.net/man/2/ptrace 176 | 177 | ## Task 1 178 | `fantasy.c`是一个简单的打印字符串的程序: 179 | ``` 180 | #include "stdio.h" 181 | 182 | int main() 183 | { 184 | printf("Oh, Fantasy!\n"); 185 | return 0; 186 | } 187 | ``` 188 | 它将打印出 `Oh, Fantasy!\n`。 189 | 190 | 现在你要做的是按照文件中的提示补全`reverse.c`中的代码(有`TODO`的地方),使得`reverse.c`在通过ptrace追踪`fantasy.c`时,一旦检测到`fantasy.c`调用了 `SYS_write` 系统调用的时候,通过修改系统调用的参数把write要输出的字符串反转,即`\n!ysatnaF ,hO`,再进行系统调用。当然,你也可以不按照`reverse.c`的模版来写,自由发挥是资瓷的。 191 | 192 | 补全好以后运行的步骤是先编译`fantasy.c`: 193 | ``` 194 | gcc fantasy.c -o fantasy 195 | ``` 196 | 然后在同一目录下编译运行`reverse.c`: 197 | ``` 198 | gcc reverse.c -o reverse 199 | ./reverse 200 | ``` 201 | 202 | ## Task 2 单步调试 203 | ### 准备知识 204 | ### GDB调试工具 205 | GDB是GNU开源组织发布的一个强大的UNIX下的程序调试工具。一般来说,GDB主要帮忙你完成下面四个方面的功能: 206 | - 启动你的程序,可以按照你的自定义的要求随心所欲的运行程序。 207 | - 可让被调试的程序在你所指定的调置的断点处停住。 208 | - 当程序被停住时,可以检查此时你的程序中所发生的事。 209 | - 动态的改变你程序的执行环境。 210 | ## Task 2 211 | 可爱的助教当然不会让你手写GDB啦。你只需要写一个简单的调试工具,可以跟踪程序执行,监控程序每一条指令的运行,并输出当前指令、指令的值、`EAX` `EBX` `ECX` `EDX` 寄存器的状态。原理是通过PTRACE_PEEKUSER 和 PTRACE_GETREGS来访问寄存器,用PTRACE_PEEKTEXT来访问内存,通过PTRACE\_SINGLESTEP来实现单步调试。 212 | 213 | `interesting.s` 是要被调试的汇编程序,他的功能是先打印`Oh, \n`,然后打印`interesting!\n`。你可以打开它来看看有哪些代码。 214 | 215 | `step.c`是不完整的单步调试工具的代码。你要做的是根据`step.c`中的要求将其补充完整,然后进行测试。`step.c`的一个输入参数`argv`是要被调试的程序路径。当然,你也可以不按照`step.c`的模版来写,自由发挥是资瓷的。 216 | 217 | 补充完整以后,首先编译一下`interesting.s`: 218 | ``` 219 | as interesting.s -o interesting.o 220 | ld -o interesting interesting.o 221 | ``` 222 | 然后编译一下`step.c`: 223 | ``` 224 | gcc step.c -o step 225 | ``` 226 | 然后使用`step`来单步调试`interesting`: 227 | ``` 228 | ./step interesting 229 | ``` 230 | 这时应该会输出这样的结果: 231 | ``` 232 | [1] RIP = 0x00000000004000b0, Instruction = 0x00000000000004b8 233 | EAX: 0x00000000 EBX: 0x00000000 234 | ECX: 0x00000000 EDX: 0x00000000 235 | Press any key to continue... 236 | ...... 237 | ``` 238 | 然后就一直按回车就行啦,可以观察到这个过程中四个寄存器的变化,看看和`interesting.s`中的是不是对应了呢: 239 | ``` 240 | ... 241 | movl $4, %eax 242 | movl $1, %ebx 243 | movl $s1, %ecx 244 | movl $5, %edx 245 | int $0x80 246 | ... 247 | ``` 248 | 然后可以运行: 249 | ``` 250 | objdump -d interesting 251 | ``` 252 | 来检查 `RIP` 和 `Instruction` 输出的是不是正确。只要对比末位是不是一样就行了,因为X86-64是CISC,指令的长度不确定,而我们固定只取64位,超出指令的部分可能会是其他指令。 253 | 254 | ## 尝试设置断点 255 | 为了让大家减轻负担,这部分不用提交。不需要你写代码,只要看看代码,跑一跑,体验一下就行了。断点的代码位于Task2文件夹里 256 | 257 | ### 通过int 3指令在调试器中设定断点 258 | 要在被调试进程中的某个目标地址上设定一个断点,调试器需要做下面两件事情: 259 | 260 | - 保存目标地址上的数据 261 | - 将`目标地址`上的第一个字节替换为int 3指令(`0xcc`) 262 | 263 | 然后,当调试器向操作系统请求开始运行进程时,进程最终一定会碰到int 3指令。此时进程停止,操作系统将发送一个信号。这时就是调试器再次出马的时候了,接收到一个其子进程(或被跟踪进程)停止的信号,然后调试器要做下面几件事: 264 | 265 | - 在目标地址上用原来的指令替换掉int 3 266 | 267 | - 将被跟踪进程中的指令指针向后递减1。这么做是必须的,因为现在指令指针指向的是已经执行过的int 3之后的下一条指令。 268 | 269 | - 由于进程此时仍然是停止的,用户可以同被调试进程进行某种形式的交互。这里调试器可以让你查看变量的值,检查调用栈等等。 270 | 271 | - 当用户希望进程继续运行时,调试器负责将断点再次加到目标地址上(由于在第一步中断点已经被移除了),除非用户希望取消断点。 272 | 273 | ### 运行方法 274 | 1. 编译`breakpoint.c`和`interesting.s` 275 | ``` 276 | gcc breakpoint.c -o breakpoint 277 | as interesting.s -o interesting.o 278 | ld -o interesting interesting.o 279 | ``` 280 | 281 | 2. 使用`objdump -d interesting`来查看汇编指令对应的指令地址,看看你想在哪停下来。输出可能长这样 282 | ``` 283 | interesting: file format elf64-x86-64 284 | 285 | 286 | Disassembly of section .text: 287 | 288 | 00000000004000b0 <_start>: 289 | 4000b0: b8 04 00 00 00 mov $0x4,%eax 290 | 4000b5: bb 01 00 00 00 mov $0x1,%ebx 291 | 4000ba: b9 e3 00 60 00 mov $0x6000e3,%ecx 292 | 4000bf: ba 05 00 00 00 mov $0x5,%edx 293 | 4000c4: cd 80 int $0x80 294 | 4000c6: b8 04 00 00 00 mov $0x4,%eax 295 | 4000cb: bb 01 00 00 00 mov $0x1,%ebx 296 | 4000d0: b9 e9 00 60 00 mov $0x6000e9,%ecx 297 | 4000d5: ba 0d 00 00 00 mov $0xd,%edx 298 | 4000da: cd 80 int $0x80 299 | 4000dc: b8 01 00 00 00 mov $0x1,%eax 300 | 4000e1: cd 80 int $0x80 301 | 302 | ``` 303 | 304 | 比如我想要在指令地址为`0x4000c6`的地方设置断点,也就是第一次系统调用(`int $0x80`)执行完后。那么运行: 305 | ``` 306 | ./breakpoint interesting 0x4000c6 307 | ``` 308 | 309 | 然后就可以看到进程在`0x4000c6`停下了: 310 | ``` 311 | target started. will run 'interesting' 312 | Child started. EIP = 0x004000b0 313 | Original data at 0x004000c6: 0x000004b8 314 | After trap, data at 0x004000c6: 0x000004cc 315 | Oh, 316 | Child got a signal: Trace/breakpoint trap 317 | Child stopped at EIP = 0x004000c6 318 | 319 | interesting! 320 | Child exited 321 | ``` 322 | 323 | ## 提交 324 | 把改好的`project1`整个文件夹压缩成`学号-姓名-project1.zip`发送到`137645534@qq.com`,邮件名`学号-姓名-project1`. 325 | 326 | ## 加油! 327 | 有任何困难或不明白的地方,请联系我。 328 | 329 | 黎先生 330 | 331 | Tel: 17621782336 332 | 333 | QQ: 137645534 334 | 335 | email: 137645534@qq.com / lijinning@sjtu.edu.cn 336 | --------------------------------------------------------------------------------