0
$\begingroup$

From my understanding, something like Linux is very geared towards C programs, with things like libc. What I'm wondering is, even in Java, you have arguments to your "main" program. The same thing occurs in C. From my understanding, this is just some data the kernel puts on an anonymous page for the stack of the process. How does this actually really work across languages, and how can the kernel/operating system support it? More detail is very appreciated, I'm very interested to hear about this.

$\endgroup$
4
  • $\begingroup$ I am not sure what you call an "anonymous page". $\endgroup$
    – user16034
    Jun 9, 2023 at 8:09
  • $\begingroup$ I think of language systems supporting (language) virtual machines, and OSs providing a virtual machine. $\endgroup$
    – greybeard
    Jun 10, 2023 at 8:22
  • $\begingroup$ Could you elaborate @greybeard ? The second is a perspective my OS professor introduced me to. $\endgroup$ Jun 10, 2023 at 17:09
  • 1
    $\begingroup$ What I wanted to stress is than in my eyes OSs do not in general, support programming languages (I'm well aware of more than one handful of exceptions). They define an interface: call it binary API (BAPI) or virtual machine. (Is this the second perspective you refer to?) Each language system supporting / supported on such a "platform" is responsible to map the execution model / virtual machine defined by the language to that BAPI. With C, that's what part of libc is about, for Java, there's the JVM/JRE. $\endgroup$
    – greybeard
    Jun 10, 2023 at 18:20

1 Answer 1

0
$\begingroup$

I'm glossing over a few (lots of) details, but the following is essentially what happens when you run a program:

  1. The operating system breaks off a chunk of memory for the program to run in. The binary code is copied into part of that memory (possibly a segment marked as read-only by non-OS processes). Another part is reserved for global variables, and another for local variables.
  2. There exists a program counter that points to the part of that memory containing the instruction that will run next. This is a dedicated register that is read by the CPU directly when it executes the next cycle.
  3. There exists a stack pointer that points to the beginning of the part of memory reserved for local variables. When we add a new local variable (including function arguments), we put it at the current stack pointer, and then increment the stack pointer, thus creating a stack. (If we add too many locals, e.g. via a recursive call that never terminates, we run off the end of the stack: that's what a stack overflow is.)
  4. When we call a function, we:
    1. push the current value of the program counter onto the stack. This lets us know where to return to.
    2. push each of the function arguments onto the stack
    3. increment the stack pointer to reserve space for the function's local variables. (The part of the stack corresponding to a particular function call is called its stack frame).
    4. Set the program counter to the location of the function we want to call.
    5. The return statement compiles to code that undoes all that and sets the program counter to the value we stored on the stack.
  5. So all the operating system has to do to pass the command line arguments is:
    1. The shell (command prompt) is just a regular program. You type a command; it reads it, tokenizes the arguments, and puts them in an array.
    2. The shell calls a couple of system calls (functions that talk to the operating system) in the fork and exec family (for Unix. Other OSes have different names for them). Those tell the operating system to start a new process with the supplied arguments. (The path to the program to be run is put at the front of the argument array)
    3. The operating system reserves space for the program (as described in (1)), puts that array on the stack just like any other function call would do, and sets the program counter to the location of main. (The return address will be the location of some cleanup code that frees all the memory and such). main now has access to the array just like any other function would have access to its own arguments.

There will also be some extra steps involved in linking shared libraries, setting up multiprocessing, and doing memory protection. I didn't explain how these work because I don't know they're out of scope.

$\endgroup$
8
  • $\begingroup$ Pretty good summary of the things I do understand. Perhaps I wasn't specific, but my question is specifically about part 5.3. I want to know how that works. And also, do all languages thus have to use something called a "stack" for function calls? $\endgroup$ Jun 10, 2023 at 0:21
  • $\begingroup$ There's not much more too it. Suppose the shell just called exec(program_path, arg0, arg1). (where arg0 and arg1 are the command line arguments that the shell read and tokenized). Suppose that SP and PC are the stack pointer and program counter, and push pushes to the stack. The operating system will then do something like this (psuedocode): process_mem = allocate(N); memcpy(process_mem, binary_code); SP = process_memory + offset; push PC + 1 /* return code*/; push dup(program_path); push dup(arg0); push dup(arg1); PC = process_mem; Basically like any other function call. $\endgroup$
    – Ray
    Jun 11, 2023 at 2:50
  • $\begingroup$ Was there a particular part of that that you want me to go into more detail on? As for the second question, it's not officially required that all languages use a stack for function calls, but you'd have a hell of a time supporting recursion if you did it any other way, and it's been the standard approach for so long that many assembly languages have built in instructions for doing it that way. (e.g. you wouldn't really do push PC + 1; PC = process_mem; you'd do something like call process_mem, and it'd handle all those details (as well as a few others I glossed over). $\endgroup$
    – Ray
    Jun 11, 2023 at 2:53
  • $\begingroup$ For non-psuedocode, https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/fs/exec.c#n2030 is the actual Linux code for doing this. You'll have to follow a few function calls to get the whole picture. bprm_execve is where the new process is started (and I don't really understand what's happening in it), but the command line arguments get copied in do_execveat_common, and that part's pretty straightforward. copy_strings is a bit more complicated than a normal strcpy, but only because it needs to make sure the target page isn't swapped out to virtual memory. $\endgroup$
    – Ray
    Jun 11, 2023 at 3:15
  • $\begingroup$ Ah. The reason I was confused is that I thought with demand paging that the kernel only loads the code page in first (this is what I learned) and the rest are paged in. I was just a bit confused by this because to me it seems that it would be an inconvenience for the kernel to zero out a memory page (does it even zero it out?) before loading the argv and such onto it upon the first reference to the stack. $\endgroup$ Jun 11, 2023 at 18:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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