How many ways do you know to exit the program in C language?

Foreword

In this article, I will mainly introduce some uncommon features in C language, such as setting the function we want to execute before and after the main function, and various fancy ways to exit the program.

1. Is the main function the first and last function to be executed?

1) C language construction and destructor

Usually when we write a C program, we start from the main function, so no one of us may have cared about this problem. In fact, the main function is not the first function executed by the program, nor is it The last function executed by the program.

#include <stdio.h>
 
void __attribute__((constructor)) init1() {
  printf("before main funciton\
");
}
 
int main() {
  printf("this is main funciton\
");
}

We compile the above code and execute it, the output is as follows:

?code git:(main) ./init.out
before main funciton
this is main funciton

It can be seen that the main function is not the first function to be executed, so what is the function that the program executes for the first time? It’s very simple. Let’s take a look at the call stack of the program.

From the above results, we can know that the first function executed by the program is _start, which is the first function executed on a Unix-like operating system.

So is the main function the last function executed by the program? Let’s look at the following code:

#include <stdio.h>
 
void __attribute__((destructor)) __exit() {
  printf("this is exit\
");
}
 
void __attribute__((constructor)) init() {
  printf("this is init\
");
}
 
 
int main() {
  printf("this is main\
");
  return 0;
}

The output of the above program is as follows:

? code git:(main) ./out.out
this is init
this is main
this is exit

It can be seen that the main function is not the last function we execute! In fact, in addition to the above methods, we can also register some functions in libc, so that the program executes these functions after the main function and before exiting execution.

2) on_exit and atexit functions

We can use the above two functions to register the function, let the program execute the function we specify before exiting

#include <stdio.h>
#include <stdlib.h>
 
void __attribute__((destructor)) __exit() {
  printf("this is exit\
");
}
 
void __attribute__((constructor)) init() {
  printf("this is init\
");
}
 
void on__exit() {
  printf("this in on exit\
");
}
 
void at__exit() {
  printf("this in at exit\
");
}
 
int main() {
  on_exit(on__exit, NULL);
  atexit(at__exit);
  printf("this is main\
");
  return 0;
}
this is init
this is main
this in at exit
this in on exit
this is exit

We can carefully analyze the order in which the above program is executed. First, execute the constructor, then execute the function registered by atexit, then execute the function registered by on_exit, and finally execute the destructor. From the output of the above program, we can know that the function we registered has taken effect, but we need to pay attention to one problem, the function that is registered first will be executed first, whether it is using the atexit or on_exit function. We now look at the following code:

#include <stdio.h>
#include <stdlib.h>
 
void __attribute__((destructor)) __exit() {
  printf("this is exit\
");
}
 
void __attribute__((constructor)) init() {
  printf("this is init\
");
}
 
void on__exit() {
  printf("this in on exit\
");
}
 
void at__exit() {
  printf("this in at exit\
");
}
 
int main() {
  // Reverse the order of the following two lines
  atexit(at__exit);
  on_exit(on__exit, NULL);
  printf("this is main\
");
  return 0;
}

The above code outputs the following:

this is init
this is main
this in on exit
this in at exit
this is exit

From the output results, it is indeed the same as the rules we mentioned above, the function is registered first and then executed. This point is also mentioned in the Linux Programmer Development Manual.

But one thing to note here is that we should use the atexit function as much as possible instead of the on_exit function, because the atexit function is specified by the standard, but on_exit is not specified by the standard.

3) exit and _exit functions

Among them, the exit function is a function provided by libc. We can use this function to terminate the execution of the program normally, and the functions we registered before can still be executed. For example in the following code:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
 
void __attribute__((destructor)) __exit1() {
  printf("this is exit1\
");
}
 
void __attribute__((destructor)) __exit2() {
  printf("this is exit2\
");
}
 
 
void __attribute__((constructor)) init1() {
  printf("this is init1\
");
}
 
 
void __attribute__((constructor)) init2() {
  printf("this is init2\
");
}
 
void on__exit1() {
  printf("this in on exit1\
");
}
 
void at__exit1() {
  printf("this in at exit1\
");
}
 
void on__exit2() {
  printf("this in on exit2\
");
}
 
void at__exit2() {
  printf("this in at exit2\
");
}
 
 
int main() {
  // _exit(1);
  on_exit(on__exit1, NULL);
  on_exit(on__exit2, NULL);
  atexit(at__exit1);
  atexit(at__exit2);
  printf("this is main\
");
  exit(1);
  return 0;
}

The execution result of the above function is as follows:

You can see that our code is executed normally.

But _exit is a system call. When this method is executed, the program will be terminated directly. Let’s look at the following code:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
 
void __attribute__((destructor)) __exit1() {
  printf("this is exit1\
");
}
 
void __attribute__((destructor)) __exit2() {
  printf("this is exit2\
");
}
 
 
void __attribute__((constructor)) init1() {
  printf("this is init1\
");
}
 
 
void __attribute__((constructor)) init2() {
  printf("this is init2\
");
}
 
void on__exit1() {
  printf("this in on exit1\
");
}
 
void at__exit1() {
  printf("this in at exit1\
");
}
 
void on__exit2() {
  printf("this in on exit2\
");
}
 
void at__exit2() {
  printf("this in at exit2\
");
}
 
 
int main() {
  // _exit(1);
  on_exit(on__exit1, NULL);
  on_exit(on__exit2, NULL);
  atexit(at__exit1);
  atexit(at__exit2);
  printf("this is main\
");
  _exit(1); // only changed this function from exit to _exit
  return 0;
}

The output of the above code is as follows:

It can be seen that neither the registered function nor the final destructor is executed, and the program exits directly.

2. Fancy exit

In addition to the above _exit function, we can also use other methods to directly exit the program:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/syscall.h>
 
void __attribute__((destructor)) __exit1() {
  printf("this is exit1\
");
}
 
void __attribute__((destructor)) __exit2() {
  printf("this is exit2\
");
}
 
 
void __attribute__((constructor)) init1() {
  printf("this is init1\
");
}
 
 
void __attribute__((constructor)) init2() {
  printf("this is init2\
");
}
 
void on__exit1() {
  printf("this in on exit1\
");
}
 
void at__exit1() {
  printf("this in at exit1\
");
}
 
void on__exit2() {
  printf("this in on exit2\
");
}
 
void at__exit2() {
  printf("this in at exit2\
");
}
 
 
int main() {
  // _exit(1);
  on_exit(on__exit1, NULL);
  on_exit(on__exit2, NULL);
  atexit(at__exit1);
  atexit(at__exit2);
  printf("this is main\
");
  syscall(SYS_exit, 1); // same effect as _exit
  return 0;
}

In addition to the method of directly calling the function above to exit the function, we can also use inline assembly to exit the function. For example, in a 64-bit operating system, we can use the following code to exit the program:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/syscall.h>
 
void __attribute__((destructor)) __exit1() {
  printf("this is exit1\
");
}
 
void __attribute__((destructor)) __exit2() {
  printf("this is exit2\
");
}
 
 
void __attribute__((constructor)) init1() {
  printf("this is init1\
");
}
 
 
void __attribute__((constructor)) init2() {
  printf("this is init2\
");
}
 
void on__exit1() {
  printf("this in on exit1\
");
}
 
void at__exit1() {
  printf("this in at exit1\
");
}
 
void on__exit2() {
  printf("this in on exit2\
");
}
 
void at__exit2() {
  printf("this in at exit2\
");
}
 
 
int main() {
  // _exit(1);
  on_exit(on__exit1, NULL);
  on_exit(on__exit2, NULL);
  atexit(at__exit1);
  atexit(at__exit2);
  printf("this is main\
");
  asm(
    "movq $60, %%rax;"
    "movq $1, %%rdi;"
    "syscall;"
    :::"eax"
  );
  return 0;
}

The above is the assembly implementation of exiting the program on a 64-bit operating system, and the system call number for exiting the program on a 64-bit system is 60. Next, we use the assembly on the 32-bit operating system to realize the program exit, and the system call number for exiting the program on the 32-bit system is equal to 1:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/syscall.h>
 
void __attribute__((destructor)) __exit1() {
  printf("this is exit1\
");
}
 
void __attribute__((destructor)) __exit2() {
  printf("this is exit2\
");
}
 
 
void __attribute__((constructor)) init1() {
  printf("this is init1\
");
}
 
 
void __attribute__((constructor)) init2() {
  printf("this is init2\
");
}
 
void on__exit1() {
  printf("this in on exit1\
");
}
 
void at__exit1() {
  printf("this in at exit1\
");
}
 
void on__exit2() {
  printf("this in on exit2\
");
}
 
void at__exit2() {
  printf("this in at exit2\
");
}
 
 
int main() {
  // _exit(1);
  on_exit(on__exit1, NULL);
  on_exit(on__exit2, NULL);
  atexit(at__exit1);
  atexit(at__exit2);
  printf("this is main\
");
  asm volatile(
    "movl $1, %?x;"
    "movl $1, %?i;"
    "int $0x80;"
    :::"eax"
  );
  return 0;
}

3. Summary

In this article, I will mainly introduce some operations in C language and program exit. I hope you will gain something!