博客 / 詳情

返回

gcc 好玩的 builtin 函數

gcc 好玩的 builtin 函數

前言

在本篇文章當中主要想給大家介紹一些在 gcc 編譯器當中給我們提供的一些好玩的內嵌函數 (builtin function)🤣🤣🤣 。

__builtin_frame_address

使用內嵌函數實現

__builtin_frame_address(x) // 其中 x 一個整數

這個函數主要是用於得到函數的棧幀的,更具體的來説是得到函數的 rbp (如果是 x86_64 的機器,在 32 位系統上就是 ebp)的值,也就是棧幀的棧底的值。

我們現在使用一個例子來驗證測試一下:

#include <stdio.h>

void func_a()
{
  void* p = __builtin_frame_address(0);
  printf("fun_a frame address = %p\n", p);
}


int main()
{
  void* p = __builtin_frame_address(0);
  printf("main frame address = %p\n", p);
  func_a();
  return 0;
}

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

main frame address = 0x7ffcecdd7a00
fun_a frame address = 0x7ffcecdd79d0

上面輸出的結果就是每個函數的棧幀中棧底 rbp/ebp 寄存器的值,可能你會有疑問,憑什麼説這個值就是 rbp 的值😂😂😂。我們現在來證明一下,我們可以使用代碼獲取得到 rbp 的值。

使用內斂彙編實現


#include <stdio.h>
#include <sys/types.h>

u_int64_t rbp;

#define frame_address                   \
        asm volatile(                   \
          "movq %%rbp, %0;"             \
          :"=m"(rbp)::                  \
        );                              \
        printf("rbp = %p from inline assembly\n", (void*) rbp);

void bar()
{
  void* rbp = __builtin_frame_address(0);
  printf("rbp = %p\n", rbp);
  frame_address
}

int main()
{
  bar();
  return 0;
}

在上面的程序當中,我們使用一段宏可以得到寄存器 rbp 的值(在上面的代碼當中,我們使用內斂彙編得到 rbp 的值,並且將這個值存儲到變量 rbp 當中),我們將這個值和 builtin 函數的返回值進行對比,我們就可以知道返回的是不是寄存器 rbp 的值了,上面的程序執行結果如下所示:

rbp = 0x7ffe9676ac00
rbp = 0x7ffe9676ac00 from inline assembly

從上面的結果我們可以知道,內置函數返回的確實是寄存器 rbp 的值。

事實上我們除了可以獲取當前函數的棧幀之外,我們還可以獲取調用函數的棧幀,具體根據 x 的值進行確定:

  • x = 0 : 獲取當前函數的棧幀,也就是棧底的位置。
  • x = 1 : 獲取調用函數的棧幀。
  • x = 2 : 獲取調用函數的調用函數的棧幀。
  • ......

比如説下面的程序:

#include <stdio.h>

void func_a()
{
  void* p = __builtin_frame_address(1);
  printf("caller frame address = %p\n", p);
}


int main()
{
  void* p = __builtin_frame_address(0);
  printf("main frame address = %p\n", p);
  func_a();
  return 0;
}

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

main frame address = 0x7ffda7a4b460
caller frame address = 0x7ffda7a4b460

從上面的輸出結果我們可以看到當參數的值等於 1 的時候,返回的是調用函數的棧幀。

#include <stdio.h>

void func_a()
{
  printf("In func_a\n");
  void* p = __builtin_frame_address(2);
  printf("caller frame address = %p\n", p);
}

void func_b()
{
  printf("In func_b\n");
  void* p = __builtin_frame_address(1);
  printf("caller frame address = %p\n", p);

  func_a();
}


int main()
{
  void* p = __builtin_frame_address(0);
  printf("main frame address = %p\n", p);
  func_b();
  return 0;
}

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

main frame address = 0x7ffdadbe6ff0
In func_b
caller frame address = 0x7ffdadbe6ff0
In func_a
caller frame address = 0x7ffdadbe6ff0

在上方的程序當中我們在主函數調用函數 func_b ,然後在函數 func_b 當中調用函數 func_a ,我們可以看到根據參數 x 的不同,返回的棧幀的層級也是不同的,根據前面參數 x 的意義我們可以知道,他們得到的都是主函數的棧幀。

__builtin_return_address

使用內嵌函數實現

這個內嵌函數的主要作用就是得到函數的返回地址,首先我們需要知道的是,當我們進行函數調用的時候我們需要知道當這個函數執行完成之後返回到什麼地方,因為 cpu 只會一條指令一條指令的執行,我們需要告訴 cpu 下一條指令的位置,因此當我們進行函數調用的時候需要保存調用函數的 call 指令下一條指令的位置,並且將它保存在棧上,當被調用函數執行完成之後繼續回到調用函數的下一條指令的位置執行,因為我們已經將這個下一條指令的地址放到棧上了,當調用函數執行完成之後直接從棧當中取出這個值即可。

__builtin_return_address 的簽名如下:

__builtin_return_address(x) // x 是一個整數

其中 x 和前面的 __builtin_frame_address 含義相似:

  • x = 0 : 表示當前函數的返回地址。
  • x = 1 : 表示當前函數的調用函數的返回地址,比如説 main 函數調用 func_a 如果在 func_a 裏面調用這個內嵌方法,那麼返回的就是 main 函數的返回值。
  • x = 2 : 表示當前函數的調用函數的調用函數的返回地址。
#include <stdio.h>

void func_a()
{
  void* p = __builtin_return_address(0);
  printf("fun_a return address = %p\n", p);

  p = __builtin_return_address(1);
  printf("In func_a main return address = %p\n", p);
}


int main()
{
  void* p = __builtin_return_address(0);
  printf("main return address = %p\n", p);
  func_a();
  return 0;
}

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

main return address = 0x7fc5c57c90b3
fun_a return address = 0x400592
In func_a main return address = 0x7fc5c57c90b3

從上面的輸出結果我們可以知道

使用內斂彙編實現

如果我們調用一個函數的時候(在x86裏面執行 call 指令)首先會將下一條指令的地址壓棧(在 32 位系統上就是將 eip 壓棧,在 64 位系統上就是將 rip 壓棧),然後形成調用函數的棧幀。然後將 rbp 寄存器的值指向下圖當中的位置。

#include <stdio.h>
#include <sys/types.h>

#define return_address            \
    u_int64_t rbp;                \
    asm volatile(                 \
      "movq %%rbp, %0":"=m"(rbp)::\
    );                            \
    printf("From inline assembly return address = %p\n", (u_int64_t*)*(u_int64_t*)(rbp + 8));

void func_a()
{
  printf("In func_a\n");
  void* p = __builtin_return_address(0);
  printf("fun_a return address = %p\n", p);
  return_address
}

int main()
{
  printf("In main function\n");
  void* p = __builtin_return_address(0);
  printf("main return address = %p\n", p);
  return_address
  func_a();
  return 0;
}

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

In main function
main return address = 0x7fe6a7b050b3
From inline assembly return address = 0x7fe6a7b050b3
In func_a
fun_a return address = 0x4005d2
From inline assembly return address = 0x4005d2

從上面的輸出結果我們可以看到,我們自己使用內斂彙編直接得到寄存器 rbp 的和內嵌函數返回的值是一致的,這也從側面反映出來了內嵌函數的作用。在上面的代碼當中定義定義的宏 return_address 的作用就是將寄存器 rbp 的值保存到變量 rbp 當中。

除了得到當前棧幀的 rbp 的值之外我們還可以,函數的調用函數的 rbp,調用函數的調用函數的 rbp,當然可以直接使用 builtin 函數實現,除此之外我們還可以使用內斂彙編去實現這一點:

#include <stdio.h>
#include <sys/types.h>

#define return_address            \
    u_int64_t rbp;                \
    asm volatile(                 \
      "movq %%rbp, %%rcx;"        \
      "movq (%%rcx), %%rcx;"      \
      "movq %%rcx, %0;"           \
      :"=m"(rbp)::"rcx"           \
    );                            \
    printf("From inline assembly main return address = %p\n", (u_int64_t*)*(u_int64_t*)(rbp + 8));

void func_a()
{
  printf(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> In func_a\n");
  void* p = __builtin_return_address(1);
  printf("main return address = %p\n", p);
  return_address
  printf("<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< Out func_a\n");
}


int main()
{
  func_a();
  void* p = __builtin_return_address(0);
  printf("main function return address = %p\n", p);
  return 0;
}

上面的程序的輸出結果如下所示

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> In func_a
main return address = 0x7f9aec6c80b3
From inline assembly main return address = 0x7f9aec6c80b3
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< Out func_a
main function return address = 0x7f9aec6c80b3

我們可以看到上面的輸出,我們自己用內斂彙編實現的結果和 __builtin_return_address 的返回結果是一樣的,這也驗證了我們實現的正確性。要想理解上面的代碼首先我們需要理解函數調用的時候形成的棧幀,如下圖所示:

根據上圖我們可以知道在 func_a 函數當中,rbp 指向的地址存放的是上一個函數的 rbp 寄存器的值,因此我們可以使用間接尋址,找到調用 func_a 的主函數的 rbp 的值,即以在函數 func_a 當中 rbp 寄存器的值為地址,找到這個地址的值就是主函數的 rbp 的值。

至此我們已經知道了,__builtin_return_address 的返回結果是當前函數的返回地址,也就是當前函數執行完成返回之後執行的下一條指令,我們可以利用這一點做出一個非常好玩的東西,直接跳轉到返回地址執行不執行當前函數的後續代碼:

#include <stdio.h>

void func_a()
{
  void* p        = __builtin_return_address(0); // 得到當前函數的返回地址
  void* rbp      = __builtin_frame_address(0);  // 得到當前函數的棧幀的棧底
  void* last_rbp = __builtin_frame_address(1);    // 得到調用函數的棧幀的棧底
  asm volatile(
    "leaq 16(%1), %%rsp;" // 恢復 rsp 寄存器的值 ⓷
    "movq %2, %%rbp;"     // 恢復 rbp 寄存器的值 ⓸
    "jmp *%0;"            // 直接跳轉                        ⓹
    ::"r"(p), "r"(rbp), "r"(last_rbp): 
  );
  printf("finished in func_a\n"); // ①
}


int main()
{
  void* p = __builtin_return_address(0);
  printf("main return address = %p\n", p);
  func_a(); // ②
  printf("finished in main function \n");
  // 打印九九乘法表
  int i, j;
  for(i = 1; i < 10; ++i) 
  {
    for(j = 1; j <= i; ++j) {
      printf("%d x %d = %d\t", i, j, i * j);
    }
    printf("\n");
  }
  return 0;
}

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

main return address = 0x7f63e05c60b3
finished in main function 
1 x 1 = 1
2 x 1 = 2       2 x 2 = 4
3 x 1 = 3       3 x 2 = 6       3 x 3 = 9
4 x 1 = 4       4 x 2 = 8       4 x 3 = 12      4 x 4 = 16
5 x 1 = 5       5 x 2 = 10      5 x 3 = 15      5 x 4 = 20      5 x 5 = 25
6 x 1 = 6       6 x 2 = 12      6 x 3 = 18      6 x 4 = 24      6 x 5 = 30      6 x 6 = 36
7 x 1 = 7       7 x 2 = 14      7 x 3 = 21      7 x 4 = 28      7 x 5 = 35      7 x 6 = 42      7 x 7 = 49
8 x 1 = 8       8 x 2 = 16      8 x 3 = 24      8 x 4 = 32      8 x 5 = 40      8 x 6 = 48      8 x 7 = 56      8 x 8 = 64
9 x 1 = 9       9 x 2 = 18      9 x 3 = 27      9 x 4 = 36      9 x 5 = 45      9 x 6 = 54      9 x 7 = 63      9 x 8 = 72      9 x 9 = 81

從上面程序的輸出結果來看,上面的程序並沒有執行語句 ① ,但是卻執行了主函數 ② 之後的程序,並且正確輸出字符串和九九乘法表。這就相當於我們提前進行了跳轉。要想得到這樣的結果,我們只需要在函數 func_a 內部恢復上一個函數的棧幀,並且將 rip 指向函數 func_a 的返回地址即可。

上方的程序發生轉移的代碼就是那段內斂彙編代碼,在內斂彙編代碼當中我們首先恢復 main 函數的棧幀(主要是正確恢復寄存器 rbp 和 rsp )的值,然後直接跳轉到返回地址繼續執行,所以才正確執行了主函數後續的代碼。

恢復主函數的 rbp 寄存器的值很好理解,因為我們只需要通過內嵌函數直接得到即可,但是主函數的 rsp 寄存器的值可能有一點複雜,s首先我們需要知道,主函數和 func_a 的兩個與棧幀有關的寄存器的指向,他們的指向如下圖所示:

  • 根據上文的分析我們可以直接通過在函數 func_a 當中直接使用 __builtin_frame_address(1) 得到主函數的 rbp 值,然後將其直接賦值給 rbp 寄存器就可以了,我們就恢復了主函數棧底的值,對應的語句位上面代碼的 ⓸。
  • 根據上文的分析我們可以直接通過在函數 func_a 當中直接使用 __builtin_return_address(0) 得到 func_a 的返回地址,我們可以直接 jmp 到這條指令執行,但是在 jmp 之前我們需要先恢復主函數的棧幀,對應的語句位上面的 ⓹。
  • 根據上圖我們可以分析到主函數 rsp 的值就是函數 func_a 中 rbp 寄存器的值加上 16,因為 rip 和 rbp 分別佔 8 個字節,因此我們通過 ⓷ 恢復主函數的 rsp 的值。

根據上面的分析我就大致就可以理解了上述的代碼的流程了。

與二進制相關的內嵌函數

__builtin_popcount

在 gcc 內部給我們提供了很多用於比特操作的內嵌函數,比如説如果我們想統計一下一個數據二進制表示有多少個為 1 的比特位。

  • __builtin_popcount : 統計一個數據的二進制表示有多少個為 1 的比特位。
#include <stdio.h>

int main()
{
  int i = -1;
  printf("bits = %d\n", __builtin_popcount(i));
  i = 15;
  printf("bits = %d\n", __builtin_popcount(i));
  return 0;
}

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

bits = 32
bits = 4

-1 和 15 的二進制表示如下:

-1 = 1111_1111_1111_1111_1111_1111_1111_1111
15 = 0000_0000_0000_0000_0000_0000_0000_1111

因此統計一下對應數字的比特位等於 1 的個數可以知道,內嵌函數 __builtin_popcount 的輸出結果是沒錯的。

  • \_\_builtin_popcountl 和 \_\_builtin_popcountl,這兩個函數的作用和 __builtin_popcount 的作用是一樣的,但是這兩個函數是用於 long 和 long long 類型的參數。

__builtin_ctz

  • __builtin_ctz : 從右往左數,統計一個數據尾部比特位等於 0 的個數,具體是在遇到第一個 1 之前,已經遇到了幾個 1 。
#include <stdio.h>

int main()
{
  printf("%d\n", __builtin_ctz(1)); // ctz = count trailing zeros. 
  printf("%d\n", __builtin_ctz(2));
  printf("%d\n", __builtin_ctz(3));
  printf("%d\n", __builtin_ctz(4));
  return 0;
}

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

0
1
0
2

1,2,3,4 對應的二進制表示如下所示:

1 = 0000_0000_0000_0000_0000_0000_0000_0001 // 到第一個 1 之前 有 0 個 0
2 = 0000_0000_0000_0000_0000_0000_0000_0010 // 到第一個 1 之前 有 0 個 1
3 = 0000_0000_0000_0000_0000_0000_0000_0011 // 到第一個 1 之前 有 0 個 0
4 = 0000_0000_0000_0000_0000_0000_0000_0100 // 到第一個 1 之前 有 0 個 2

根據上面不同數據的二進制表示以及上方程序的輸出結果可以知道 __builtin_ctz 的輸出就是尾部等於 0 的個數。

  • \_\_builtin_ctzl 和 \_\_builtin_ctzll 與 __builtin_ctz 的作用是一樣的,但是這兩個函數是用於 long 和 long long 類型的數據。

上面談到的 __builtin_ctz 這個內嵌函數我們可以用於求一個數據的 lowbit 的值,我們知道一個數據的 lowbit 就是最低位的比特所表示的數據,他的求解函數如下:

int lowbit(int x)
{
  return (x) & (-x);
}

我們也可以使用上面的內嵌函數去實現,看下面的代碼,我們使用上面的內嵌函數定義一個宏去實現 lowbit:

#include <stdio.h>

#define lowbit(x) (1 << (__builtin_ctz(x)))

int lowbit(int x)
{
  return (x) & (-x);
}

int main()
{
  for(int i = 0; i < 16; ++i)
  {
    printf("macro = %d function = %d\n", lowbit(i), lowbit2(i));
  }
  return 0;
}

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

macro = 1 function = 0
macro = 1 function = 1
macro = 2 function = 2
macro = 1 function = 1
macro = 4 function = 4
macro = 1 function = 1
macro = 2 function = 2
macro = 1 function = 1
macro = 8 function = 8
macro = 1 function = 1
macro = 2 function = 2
macro = 1 function = 1
macro = 4 function = 4
macro = 1 function = 1
macro = 2 function = 2
macro = 1 function = 1

可以看到我們使用內嵌函數和自己定義的 lowbit 函數實現的結果是一樣的。

__builtin_clz

這個是用於統計一個數據的二進制表示,從左往右數遇到第一個比特位等於 1 之前已經遇到了多少個 0。

#include <stdio.h>

int main()
{
  for(int i = 1; i < 16; ++i) 
  {
    printf("i = %2d and result = %2d\n", i, __builtin_clz(i));
  }
  printf("i = %2d and result = %2d\n", -1, __builtin_clz(-1));
  return 0;
}

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

i =  1 and result = 31 // 高位有 31 個 0
i =  2 and result = 30 // 高位有 30 個 0
i =  3 and result = 30
i =  4 and result = 29
i =  5 and result = 29
i =  6 and result = 29
i =  7 and result = 29
i =  8 and result = 28
i =  9 and result = 28
i = 10 and result = 28
i = 11 and result = 28
i = 12 and result = 28
i = 13 and result = 28
i = 14 and result = 28
i = 15 and result = 28
i = -1 and result =  0 // 高位沒有 0

我們可以將上面的數據 i 對應他的二進制表示,就可以知道從左往右數遇到第一個等於 1 的比特位之前會有多少個 0 ,我們拿 -1 進行分析,因為在計算機當中數據的都是使用補碼進行表示,而 -1 的補碼如下所示:

-1 = 1111_1111_1111_1111_1111_1111_1111_1111

因此 高位沒有 0,所以返回的結果等於 0。

總結

在本篇文章當中主要給大家介紹一些在 gcc 當中比較有意思的內嵌函數,大家可以玩一下~~~~😂


更多精彩內容合集可訪問項目:https://github.com/Chang-LeHu...

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

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.