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

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.
The Linux Programming Interface, 2.2 The Shell, pg. 24
The Linux Programming Interface, 4.1 Overview, pg. 69