1

I am learning about how the call stack works by reverse engineering binaries using Radare2 (in a Linux environment). The ISA on the server (a P5 micro-architecture) I am analyzing binaries off of is x86 (Intel syntax).

Say I want to print a very simple string to the console by compiling a C program named hello.c in to an executable:

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

    return (0);
}

How does the CPU transfer the data containing the "Hello world!" string from the stack space reserved for the hello executable in memory to the console output?

  • what's an "output sink"? – Igor Skochinsky Apr 29 '18 at 15:55
  • BTW, please try to avoid editing questions in a way that invalidates existing answers. – Igor Skochinsky Apr 29 '18 at 15:58
  • I don't know how I am supposed to correct the question when it apparently was faulty to begin with. I am not attempting to invalidate existing answers but I have no idea how I am otherwise supposed to get this question to get at least one upvote. If you have enough questions with 0 votes or less, you are prevented from asking more questions. So what would you have me do? – Darien Springer Apr 29 '18 at 19:47
  • well, at least maybe explain what you mean by “output sink” and why you changed to it from the original title. I don’t think I’ve heard this term before. – Igor Skochinsky Apr 29 '18 at 19:55
2

To print to the terminal, a process started by the shell performs file I/O by making a write() system call using the already open stdout file descriptor inherited from the shell (the parent process).

For the process that manages this program, how is the data containing the "Hello world!" string transferred from the stack in memory to the child process of the shell?

  • this question is nonsense because the "hello world" process is the child process. The parent process is the shell, since the shell was the process that started the "hello world" process. When printing to the terminal (STDOUT), the child process is sending data to the parent process (the shell).
  • take a look at the binary and look at the disassembly of the main() function. The "hello world" string is hardcoded in the .rodata section of the binary. It is not written to the stack at any point. In main(), a pointer to its location is written to the stack as an argument to the printf function.
  • the runtime stack of a process is a space in virtual memory whose use is managed by the compiler. The stack is written to and read from by the CPU, which executes the instructions generated by the compiler. The stack does not perform any sort of action or computation; this is the purpose of the CPU. You should read the Q&A here: How are variables stored in and retrieved from the program stack?. Note that if a programming language does not permit recursion, a stack is not even required.
  • A shell is a special-purpose program designed to read commands typed by a user and execute appropriate programs in response to those commands. Such a program is sometimes known as a command interpreter.1

    When you type $ ./hello, the shell will read this command and proceed to call fork and exec to start a new process (I strongly urge you to read Differences between fork and exec).

  • Since each process occupies its own space in virtual memory and is as a result isolated from every other process, the shell and the new process started by the shell must use services provided by the kernel in order to share data. The Linux I/O model provides one way for processes to communicate with each other using file descriptors:

    All system calls for performing I/O refer to open files using a file descriptor, a (usually small) nonnegative integer. File descriptors are used to refer to all types of open files, including pipes, FIFOs, sockets, terminals, devices, and regular files. Each process has its own set of file descriptors.

    By convention, most programs expect to be able to use the three standard file descriptors listed in Table 4-1. These three descriptors are opened on the program’s behalf by the shell, before the program is started. Or, more precisely, the program inherits copies of the shell’s file descriptors, and the shell normally operates with these three file descriptors always open. (In an interactive shell, these three file descriptors normally refer to the terminal under which the shell is running.) If I/O redirections are specified on a command line, then the shell ensures that the file descriptors are suitably modified before starting the program.2

Table 4-1 Standard File Descriptors

This means that if one wishes to print to the terminal, one must make a write() system call using the already open stdout file descriptor.

Here is a simple example program to make things more clear:

#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;
}

We can retrieve the process ID of the shell via the echo $$ command (bash is assumed to be the shell here).

$ echo $$
29760

Then we compile and run the example program (which I simply called pid):

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

The parent process ID is that of the shell which started the pid process.

To see what is happening at the CPU level, here is disassembly of main() with comments explaining the relevant operations:

<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    

Here is the strace output for the example program:

$ 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 +++

Note the execve() system call on the first line and the write() system calls at the bottom.


  1. The Linux Programming Interface, 2.2 The Shell, pg. 24

  2. The Linux Programming Interface, 4.1 Overview, pg. 69

  • Edited question to clarify what I am asking. Does that make my question clearer? – Darien Springer Apr 20 '18 at 20:30
  • @DarienSpringer It does not. – julian Apr 20 '18 at 20:36
  • What is not clear? – Darien Springer Apr 20 '18 at 20:36
  • @DarienSpringer The statement "Does the stack directly transfer data from memory to the child process of the shell when a function is called to access some data in memory to print or return it to the console output? " - Each process has its own stack space, so this does not make sense. Did you read anything I wrote? – julian Apr 20 '18 at 20:38
  • Yes, I did read what you said. I have clarified my question. – Darien Springer Apr 21 '18 at 1:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.