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、计算机系统根底、算法与数据结构)常识。