数据如何从进程的内存传输到控制台屏幕?

逆向工程 x86 记忆
2021-07-01 16:04:28

我正在通过使用 Radare2(在 Linux 环境中)逆向工程二进制文件来了解调用堆栈是如何工作的。我正在分析二进制文件的服务器上的 ISA(P5 微架构)是 x86(英特尔语法)。

假设我想通过将名为hello.cin的 C 程序编译为可执行文件来将一个非常简单的字符串打印到控制台

int main()
{
    printf("Hello world!");

    return (0);
}

CPU 如何传输包含“Hello world!”的数据 从为hello内存中可执行文件保留的堆栈空间到控制台输出的字符串

1个回答

为了打印到终端,由 shell 启动的进程通过write()使用stdout从 shell(父进程)继承的已经打开的文件描述符进行系统调用来执行文件 I/O

对于管理这个程序的进程,包含“Hello world!”的数据如何?字符串从内存中的堆栈传输到shell的子进程?

  • 这个问题是无稽之谈,因为“hello world”进程是子进程。父进程是 shell,因为 shell 是启动“hello world”进程的进程。当打印到终端 ( STDOUT) 时,子进程正在向父进程(shell)发送数据。
  • 看看二进制文件,看看main()函数的反汇编“hello world”字符串被硬编码在.rodata二进制文件部分。它不会在任何时候写入堆栈。在 中main()指向其位置指针被写入堆栈作为printf函数的参数
  • 进程的运行时堆栈是虚拟内存中的一个空间,其使用由编译器管理。堆栈由 CPU 写入和读取,CPU 执行编译器生成的指令。堆栈不执行任何类型的操作或计算;这就是 CPU 的目的。您应该阅读此处的问答:变量如何存储在程序堆栈中并从程序堆栈中检索?. 请注意,如果编程语言不允许递归,则甚至不需要堆栈。
  • 是设计用于读取由用户输入的命令,并响应于这些命令执行适当的程序的专用程序。这样的程序有时被称为命令解释器1

    当您键入时$ ./hello,shell 将读取此命令并继续调用forkexec启动一个新进程(我强烈建议您阅读fork 和 exec 之间的差异)。

  • 由于每个进程在虚拟内存中占用自己的空间,因此与其他进程隔离,因此外壳程序和外壳程序启动的新进程必须使用内核提供的服务才能共享数据Linux I/O 模型为进程之间使用文件描述符提供了一种通信方式

    所有用于执行 I/O 的系统调用都使用文件描述符(一个(通常很小)非负整数)引用打开的文件。文件描述符用于指代所有类型的打开文件,包括管道、FIFO、套接字、终端、设备和常规文件。每个进程都有自己的一组文件描述符。

    按照惯例,大多数程序都希望能够使用表 4-1 中列出的三个标准文件描述符。在程序启动之前,这三个描述符由外壳程序代表程序打开。或者,更准确地说,程序继承了外壳文件描述符的副本,外壳通常在这三个文件描述符始终打开的情况下运行。(在交互式 shell 中,这三个文件描述符通常指的是运行 shell 的终端。)如果在命令行上指定了 I/O 重定向,那么 shell 会确保在启动程序之前适当地修改文件描述符. 2

表 4-1 标准文件描述符

这意味着如果希望打印到终端,则必须write()使用已打开的stdout文件描述符进行系统调用

这是一个简单的示例程序,使事情更清楚:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main(void) {
        pid_t current_PID = getpid();
        pid_t parent_PID = getppid();

        printf("Current process ID: %d\n", current_PID);
        printf("Parent process ID: %d\n", parent_PID);

        return 0;
}

我们可以通过echo $$命令获取shell的进程ID bash这里假设是shell)。

$ echo $$
29760

然后我们编译并运行示例程序(我简称为pid):

$ ./pid 
Current process ID: 9071
Parent process ID: 29760

父进程 ID 是启动该pid进程的 shell 的 ID

要查看 CPU 级别发生的情况,这里是反汇编main()并解释相关操作的注释:

<main>:
push   %ebp
mov    %esp,%ebp
and    $0xfffffff0,%esp
sub    $0x20,%esp
call   8048340 <getpid@plt>  # libc wrapper around getpid() system call
mov    %eax,0x18(%esp)       # write return value (PID) in register to stack
call   8048370 <getppid@plt> # libc wrapper around getppid() system call
mov    %eax,0x1c(%esp)       # write return value (PPID) in register to stack
mov    0x18(%esp),%eax       # read from stack, write to register
mov    %eax,0x4(%esp)        # write register value to stack as 2nd arg to printf (PID)
movl   $0x8048560,(%esp)     # read format string from memory, write to stack as 1st arg to printf
call   8048330 <printf@plt>  # libc wrapper around write() system call
mov    0x1c(%esp),%eax       # read PPID saved on stack, write to register
mov    %eax,0x4(%esp)        # write PPID in register to stack as 2nd arg to printf
movl   $0x8048578,(%esp)     # read format string from memory, write to stack as 1st arg to printf
call   8048330 <printf@plt>  # libc wrapper around write() system call
mov    $0x0,%eax
leave  
ret    

这是strace示例程序输出:

$ strace ./pid
execve("./pid", ["./pid"], [/* 53 vars */]) = 0
[ Process PID=10017 runs in 32 bit mode. ]
brk(0)                                  = 0x943d000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xfffffffff7793000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=155012, ...}) = 0
mmap2(NULL, 155012, PROT_READ, MAP_PRIVATE, 3, 0) = 0xfffffffff776d000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
open("/lib/i386-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0P\234\1\0004\0\0\0"..., 512) = 512
fstat64(3, {st_mode=S_IFREG|0755, st_size=1763068, ...}) = 0
mmap2(NULL, 1772156, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xfffffffff75bc000
mmap2(0xf7767000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1aa000) = 0xfffffffff7767000
mmap2(0xf776a000, 10876, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xfffffffff776a000
close(3)                                = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xfffffffff75bb000
set_thread_area(0xffc17c00)             = 0
mprotect(0xf7767000, 8192, PROT_READ)   = 0
mprotect(0x8049000, 4096, PROT_READ)    = 0
mprotect(0xf77b8000, 4096, PROT_READ)   = 0
munmap(0xf776d000, 155012)              = 0
getpid()                                = 10017
getppid()                               = 10014
fstat64(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 13), ...}) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xfffffffff7792000
write(1, "Current process ID: 10017\n", 26Current process ID: 10017
) = 26
write(1, "Parent process ID: 10014\n", 25Parent process ID: 10014
) = 25
exit_group(0)                           = ?
+++ exited with 0 +++

注意execve()第一行的write()系统调用和底部系统调用。


  1. Linux 编程接口,2.2 外壳,pg。24

  2. Linux 编程接口,4.1 概述,pg。69