3

The C standard states that...

...a return from the initial call to the main function is equivalent to calling the exit function with the value returned by the main function as its argument.

From what I see, this is usually implemented by the C runtime support code (crt0.c) by doing exactly this -- calling exit with the return value from main.

glibc:

result = main (argc, argv, __environ MAIN_AUXVEC_PARAM);
exit (result);

Ulysses Libc:

exit(main(argc, argv, envp));

However, when I write my own version of exit, it does not get called:

#include <stdio.h>
#include <stdlib.h>

void exit( int rc )
{
    puts( "ok" );
    fflush( stdout );
}

int main()
{
    return 0;
}

This does not yield the "ok" output I expected. Apparently I am missing something here?


Context: I am implementing a C standard library, just the ISO part, i.e. no crt0.c. I hoped that the existing system runtime would call my own exit implementation, so "my" cleanup (like flushing and closing streams, handling functions registered with atexit etc.) would automatically run on return from a main linked to my library. Obviously this is not the case, I just do not understand why not.

DevSolar
  • 67,862
  • 21
  • 134
  • 209
  • Some of the functionality of the "standard library" is usually split between the library itself and the compiler and its support object and library files. The `exit` and `atexit` functions is usually a thing that is handled by the compiler and its support files rather than the standard library. – Some programmer dude Jul 03 '19 at 06:08
  • 1
    This is simply because the run time is already linked against the original `exit` implementation. Most probably it is inlined into the `_start` function. – Ajay Brahmakshatriya Jul 03 '19 at 06:17
  • How can you write your own version of `exit` and include stdlib.h at the same time? Is this your own version of it, that is stdlib.c or equivalent? – Lundin Jul 03 '19 at 06:28
  • @Lundin: Note the context given at the end. [The real code](https://rootdirectory.ddns.net/websvn/filedetails.php?repname=PDCLib&path=%2Ftrunk%2Ffunctions%2Fstdlib%2Fexit.c) is my own `exit`, *and* my own ``. The code in the question was merely for demonstration purposes. – DevSolar Jul 03 '19 at 06:30
  • 1
    Yeah I read that but it isn't obvious if this .c file is your library implementation or some random test file. I wouldn't expect your std lib to contain main(), for example. – Lundin Jul 03 '19 at 06:32
  • @Lundin: My lib doesn't contain a `main`, but my test drivers (linking my lib) do, and that is when I realized my `exit` (where I do the calling of `atexit`-registered functions, which includes one for flushing / closing open streams) doesn't get called. – DevSolar Jul 03 '19 at 06:37

1 Answers1

5

If I understand correctly, you are trying to implement the functions in the C standard library while trying to use some parts of the C runtime (namely what calls the main function and exit).

The part of the code that usually does this is the _start function. This is typically the entry point for ELF binaries with the Linux loader.

This _start function is defined in the C runtime that your compiler is using and the call to exit is already linked (the address patched into the callsite). It is likely that it is simply inlined into the _start function.

To get the _start function to call your exit, what you have to do is to redefine the _start itself. Then you have to make sure that the C runtime's _start is not used.

I would go with something like this -

// Assuming your have included files that declare the puts and fflush functions and the stdout macro. 
int main(int argc, char* argv[]); // This is here just so that there is a declaration before the call
void _start(void) {
    char *argv[] = {"executable"}; // There is a way to get the real arguments, but I think you will have to write some assembly for that. 

    int return_value = main(1, argv);
    exit(return_value);
    // Control should NEVER reach here, because you cannot return from the _start function
}

void exit(int ret) {
    puts("ok"); 
    fflush(stdout); // Assuming your runtime defines these functions and stdout somewhere. 
    // To simulate an exit, we will just spin infinitely - 
    while(1);
}

int main(int argc, char* argv[]) {
    puts("hello world\n");
    return 0;
}

Now you can compile and link the file as -

gcc test.c -o executable -nostdlib

The -nostdlib tells the linker to not link against the standard runtime which has the implementation of _start.

Now you can execute your executable and it will call your "exit" as you expected and will then keep on looping forever. You can kill it by pressing ctrl+c or sending SIGKILL some other way.

Appendix

Just for completeness' sake, I also tried to write down the implementation for the rest of the functions.

You can firstly add the following declarations and definitions at the top of your code.

#define stdout (1)
int puts(char *s);
long unsigned int strlen(const char *s) {
        int len = 0;
        while (s[len])
                len++;
        return len;
}
int fflush(int s) {
}
void exit(int n);

strlen is defined as expected and fflush is a no-op because we are not implementing buffering for our stdio functions.

Now in a separate file puts.s write the following assembly (assuming x64 linux. Change the syscall numbers and arguments if your platform is different).

        .text
        .globl puts
puts:
        movq    %rdi, %r12
        callq    strlen
        movq    $1, %rdi
        movq    %r12, %rsi
        movq    %rax, %rdx
        movq    $1, %rax
        syscall
        retq

This is a simplest implementation of puts which calls the strlen function followed by the write syscall.

You can now compile and link everything as -

gcc test.c put.s -o executable -nostdlib

When I run the produced executable, I get the following output -

hello world
ok

and then the process just hangs. I can kill it by pressing ctrl+c.

Community
  • 1
  • 1
Ajay Brahmakshatriya
  • 8,993
  • 3
  • 26
  • 49
  • Yep this must be it. OP probably has to start with writing their own crt before anything else. Looking at one random crt for ARM that I work with now, they actually wrote the exit function in inline asm as part of the crt. – Lundin Jul 03 '19 at 06:30
  • @Lundin: That is what I was hoping to avoid -- since the crt isn't part of what ISO C specifies, I was hoping I could "plug in" to the existing framework at the calling of `main`, without "encroaching" on the platform-specifics of crt0.c... – DevSolar Jul 03 '19 at 06:36
  • @DevSolar you cannot just plug it in because it is statically linked with the rest of the ISO C part of the C runtime. After all that code expects to always run with the ISO C functions that came with it. – Ajay Brahmakshatriya Jul 03 '19 at 06:37
  • @DevSolar I think it is only the `exit` function that's a special snowflake, every other function including `atexit` should be separate from the crt. Though it is worth checking how `memcpy` and `memset` are implemented too - these will be needed by the crt for initialization of `.data` and `.bss`, so they are likely special cases as well. – Lundin Jul 03 '19 at 06:39
  • @Lundin: Well, to put it differently -- if my own `exit` doesn't get called, how to I plug in the unwinding of my `atexit` function stack into existing CRT's, without having to implement CRT's for every platform I'd like to support? ;-) Apparently the issue is that crt0.c is *statically* linked with the libc (on the Linuxes I am testing on at least), so there's no "complete" replacing the system libc at that point and I have to rely on the people behind the respective CRTs to actually link to my library to get "full support" from the CRT. Bugger. – DevSolar Jul 03 '19 at 06:45
  • It *is* part of what ISO C specifies, not just the functions. BTW @DevSolar I suggest that you check the osdev.org articles on making a proper cross-compiler, you'd really want to configure your own crt0 into it. – Antti Haapala -- Слава Україні Jul 03 '19 at 06:47
  • @AnttiHaapala: ISO C says nothing about `_start`. I have fully functional implementations of most of libc. I was looking for a way to plug in my post-`main` cleanup to the *existing* CRT, *without* reimplementing it. The valuable part of your answer, to that end, is the observation that `exit` is (probably) *statically* linked to crt0.o, so the answer is effectively "cannot be done from userspace". Or perhaps I need to register *my* `exit` with the *system's* `atexit`, so that the *system* `exit` linked to crt0.o calls the *system* `atexit` processing calling *my* `exit`... messy. – DevSolar Jul 03 '19 at 06:54
  • @AnttiHaapala: The reference to OSDev's Cross Compiler article is funny, because I have *written* that article back in the day. :-D – DevSolar Jul 03 '19 at 06:55
  • @DevSolar IIRC back in days I couldn't get my cross-compiler quite linking executables properly against my newlib port using that, so it might be that you too have missed something ;) – Antti Haapala -- Слава Україні Jul 03 '19 at 07:04
  • @DevSolar btw what I mean with ISO C is that ISO C does not specify that there is a C library that exists in *isolation* from the compiler. They're all part of the same implementation. If you're doing a new C library to compile hosted programs then in fact you're providing a new C implementation that is the compiler with your CRT. – Antti Haapala -- Слава Україні Jul 03 '19 at 07:08
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/195910/discussion-between-devsolar-and-antti-haapala). – DevSolar Jul 03 '19 at 08:16