C語言中這麼騷的退出程式的方式你知道幾個?

2022-10-19 06:00:52

C語言中這麼騷的退出程式的方式你知道幾個?

前言

在本篇文章當中主要給大家介紹C語言當中一些不常用的特性,比如在main函數之前和之後設定我們想要執行的函數,以及各種花式退出程式的方式。

main函數是最先執行和最後執行的函數嗎?

C語言構造和解構函式

通常我們在寫C程式的時候都是從main函數開始寫,因此我們可能沒人有關心過這個問題,事實上是main函數不是程式第一個執行的函數,也不是程式最後一個執行的函數。

#include <stdio.h>

void __attribute__((constructor)) init1() {
  printf("before main funciton\n");
}

int main() {
  printf("this is main funciton\n");
}

我們編譯上面的程式碼然後執行,輸出結果如下圖所示:

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

由此可見main函數並不是第一個被執行的函數,那麼程式第一次執行的函數是什麼呢?很簡單我們看一下程式的呼叫棧即可。

從上面的結果可以知道,程式第一個執行的函數是_start,這是在類Unix作業系統上執行的第一個函數。

那麼main函數是程式執行的最後一個函數嗎?我們看下面的程式碼:

#include <stdio.h>

void __attribute__((destructor)) __exit() {
  printf("this is exit\n");
}

void __attribute__((constructor)) init() {
  printf("this is init\n");
}


int main() {
  printf("this is main\n");
  return 0;
}

上面程式的輸出結果如下:

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

由此可見main函數也不是我們最後執行的函數!事實上我們除了上面的方法之外我們也可以在libc當中註冊一些函數,讓程式在main函數之後,退出執行前執行這些函數。

on_exit和atexit函數

我們可以使用上面兩個函數進行函數的註冊,讓程式退出之前執行我們指定的函數

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

void __attribute__((destructor)) __exit() {
  printf("this is exit\n");
}

void __attribute__((constructor)) init() {
  printf("this is init\n");
}

void on__exit() {
  printf("this in on exit\n");
}

void at__exit() {
  printf("this in at exit\n");
}

int main() {
  on_exit(on__exit, NULL);
  atexit(at__exit);
  printf("this is main\n");
  return 0;
}
this is init
this is main
this in at exit
this in on exit
this is exit

我們可以仔細分析一下上面程式執行的順序。首先是執建構函式,然後執行 atexit 註冊的函數,再執行 on_exit 註冊的函數,最後執行解構函式。從上面程式的輸出我們可以知道我們註冊的函數生效了,但是需要注意一個問題,先註冊的函數後執行,不管是使用 atexit 還是 on_exit 函數。我們現在看下面的程式碼:

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

void __attribute__((destructor)) __exit() {
  printf("this is exit\n");
}

void __attribute__((constructor)) init() {
  printf("this is init\n");
}

void on__exit() {
  printf("this in on exit\n");
}

void at__exit() {
  printf("this in at exit\n");
}

int main() {
  // 調換下面兩行的順序
  atexit(at__exit);
  on_exit(on__exit, NULL);
  printf("this is main\n");
  return 0;
}

上面的程式碼輸出如下:

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

從輸出的結果看確實和上面我們提到的規則一樣,先註冊的函數後執行。這一點再linux程式設計師開發手冊裡面也提到了。

但是這裡有一點需要注意的是我們應該儘可能使用atexit函數,而不是使用on_exit函數,因為atexit函數是標準規定的,而on_exit並不是標準規定的。

exit和_exit函數

其中exit函數是libc給我們提供的函數,我們可以使用這個函數正常的終止程式的執行,而且我們在前面註冊的函數還是能夠被執行。比如在下面的程式碼當中:

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

void __attribute__((destructor)) __exit1() {
  printf("this is exit1\n");
}

void __attribute__((destructor)) __exit2() {
  printf("this is exit2\n");
}


void __attribute__((constructor)) init1() {
  printf("this is init1\n");
}


void __attribute__((constructor)) init2() {
  printf("this is init2\n");
}

void on__exit1() {
  printf("this in on exit1\n");
}

void at__exit1() {
  printf("this in at exit1\n");
}

void on__exit2() {
  printf("this in on exit2\n");
}

void at__exit2() {
  printf("this in at exit2\n");
}


int main() {
  // _exit(1);
  on_exit(on__exit1, NULL);
  on_exit(on__exit2, NULL);
  atexit(at__exit1);
  atexit(at__exit2);
  printf("this is main\n");
  exit(1);
  return 0;
}

上面的函數執行結果如下所示:

this is init1
this is init2
this is main
this in at exit2
this in at exit1
this in on exit2
this in on exit1
this is exit2
this is exit1

可以看到我們的程式碼被正常執行啦。

但是_exit是一個系統呼叫,當執行這個方法的時候程式會被直接終止,我們看下面的程式碼:

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

void __attribute__((destructor)) __exit1() {
  printf("this is exit1\n");
}

void __attribute__((destructor)) __exit2() {
  printf("this is exit2\n");
}


void __attribute__((constructor)) init1() {
  printf("this is init1\n");
}


void __attribute__((constructor)) init2() {
  printf("this is init2\n");
}

void on__exit1() {
  printf("this in on exit1\n");
}

void at__exit1() {
  printf("this in at exit1\n");
}

void on__exit2() {
  printf("this in on exit2\n");
}

void at__exit2() {
  printf("this in at exit2\n");
}


int main() {
  // _exit(1);
  on_exit(on__exit1, NULL);
  on_exit(on__exit2, NULL);
  atexit(at__exit1);
  atexit(at__exit2);
  printf("this is main\n");
  _exit(1); // 只改了這個函數 從 exit 變成 _exit
  return 0;
}

上面的程式碼輸出結果如下所示:

this is init1
this is init2
this is main

可以看到我們註冊的函數和最終的解構函式都沒有被執行,程式直接退出啦。

花式退出

出了上面的_exit函數之外,我們還可以使用其他的方式直接退出程式:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/syscall.h> 

void __attribute__((destructor)) __exit1() {
  printf("this is exit1\n");
}

void __attribute__((destructor)) __exit2() {
  printf("this is exit2\n");
}


void __attribute__((constructor)) init1() {
  printf("this is init1\n");
}


void __attribute__((constructor)) init2() {
  printf("this is init2\n");
}

void on__exit1() {
  printf("this in on exit1\n");
}

void at__exit1() {
  printf("this in at exit1\n");
}

void on__exit2() {
  printf("this in on exit2\n");
}

void at__exit2() {
  printf("this in at exit2\n");
}


int main() {
  // _exit(1);
  on_exit(on__exit1, NULL);
  on_exit(on__exit2, NULL);
  atexit(at__exit1);
  atexit(at__exit2);
  printf("this is main\n");
  syscall(SYS_exit, 1); // 和 _exit 效果一樣
  return 0;
}

出了上面直接呼叫函數的方法退出函數,我們還可以使用內聯組合退出函數,比如在64位元作業系統我們可以使用下面的程式碼退出程式:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/syscall.h> 

void __attribute__((destructor)) __exit1() {
  printf("this is exit1\n");
}

void __attribute__((destructor)) __exit2() {
  printf("this is exit2\n");
}


void __attribute__((constructor)) init1() {
  printf("this is init1\n");
}


void __attribute__((constructor)) init2() {
  printf("this is init2\n");
}

void on__exit1() {
  printf("this in on exit1\n");
}

void at__exit1() {
  printf("this in at exit1\n");
}

void on__exit2() {
  printf("this in on exit2\n");
}

void at__exit2() {
  printf("this in at exit2\n");
}


int main() {
  // _exit(1);
  on_exit(on__exit1, NULL);
  on_exit(on__exit2, NULL);
  atexit(at__exit1);
  atexit(at__exit2);
  printf("this is main\n");
  asm(
    "movq $60, %%rax;"
    "movq $1, %%rdi;"
    "syscall;"
    :::"eax"
  );
  return 0;
}

上面是在64位元作業系統退出程式的組合實現,在64為系統上退出程式的系統呼叫號為60。下面我們使用32位元作業系統上的組合實現程式退出,在32位元系統上退出程式的系統呼叫號等於1:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/syscall.h>

void __attribute__((destructor)) __exit1() {
  printf("this is exit1\n");
}

void __attribute__((destructor)) __exit2() {
  printf("this is exit2\n");
}


void __attribute__((constructor)) init1() {
  printf("this is init1\n");
}


void __attribute__((constructor)) init2() {
  printf("this is init2\n");
}

void on__exit1() {
  printf("this in on exit1\n");
}

void at__exit1() {
  printf("this in at exit1\n");
}

void on__exit2() {
  printf("this in on exit2\n");
}

void at__exit2() {
  printf("this in at exit2\n");
}


int main() {
  // _exit(1);
  on_exit(on__exit1, NULL);
  on_exit(on__exit2, NULL);
  atexit(at__exit1);
  atexit(at__exit2);
  printf("this is main\n");
  asm volatile(
    "movl $1, %%eax;"
    "movl $1, %%edi;"
    "int $0x80;"
    :::"eax"
  );
  return 0;
}

總結

在本篇文章當中主要給大家介紹C語言當中一些與程式退出的騷操作,希望大家有所收穫!

以上就是本篇文章的所有內容了,我是LeHung,我們下期再見!!!更多精彩內容合集可存取專案:https://github.com/Chang-LeHung/CSCore

關注公眾號:一無是處的研究僧,瞭解更多計算機(Java、Python、計算機系統基礎、演演算法與資料結構)知識。