故事简述
这里主要想解释的是两个现象:
test_setvbuf.c
中fputs
被改写为fwrite
strlen
会被直接优化为具体的大小
两个现象其实是一个问题,这里以strlen
来阐述说明。
问题分析
先看代码:
char a[10]={'x',};
lena = strlen(a);
printf("%s,%d\r\n",a,lena);
for (i = 0; i < 10; i++)
{
a[i] = i+1;
}
lena = strlen(a);
printf("%s,%d\r\n",a,lena);
里面有两个长度strlen
:
- 第一个长度计算的时候,并没有
bl <strlen>
,而是直接返回了长度1 - 第二个长度计算的时候,是有
bl <strlen>
最开始,我们以为是优化级别的原因,于是把o2
改成了o0
,结果还是同样的问题。
这让人非常疑惑,编译器也太智能了吧,但为啥我都不让你帮我优化了,你还是要优化呢?
这里就得从gcc
着手了,于是我们让智谱清言来分析一下它的配置:
Target: aarch64-none-elf
Configured with: /data/jenkins/workspace/GNU-toolchain/arm-10-4/src/gcc/configure --target=aarch64-none-elf --prefix=/data/jenkins/workspace/GNU-toolchain/arm-10-4/build-mingw-aarch64-none-elf/install// --with-gmp=/data/jenkins/workspace/GNU-toolchain/arm-10-4/build-mingw-aarch64-none-elf/host-tools --with-mpfr=/data/jenkins/workspace/GNU-toolchain/arm-10-4/build-mingw-aarch64-none-elf/host-tools --with-mpc=/data/jenkins/workspace/GNU-toolchain/arm-10-4/build-mingw-aarch64-none-elf/host-tools --with-isl=/data/jenkins/workspace/GNU-toolchain/arm-10-4/build-mingw-aarch64-none-elf/host-tools --disable-shared --disable-nls --disable-threads --disable-tls --enable-checking=release --enable-languages=c,c++,fortran --with-newlib --with-libiconv-prefix=/data/jenkins/workspace/GNU-toolchain/arm-10-4/build-mingw-aarch64-none-elf/host-tools --host=i686-w64-mingw32 --with-pkgversion='GNU Toolchain for the A-profile Architecture 10.3-2021.07 (arm-10.29)' --with-bugurl=https://bugs.linaro.org/
Thread model: single
Supported LTO compression algorithms: zlib
gcc version 10.3.1 20210621 (GNU Toolchain for the A-profile Architecture 10.3-2021.07 (arm-10.29))
这里我们看到了一个参数:
--with-newlib
: 使用Newlib作为标准库。
nostdlib用法
于是问道:如果配置 gcc时使用了–with-newlib,那么在使用strlen的时候,是使用gcc的呢,还是自己写好的strlen呢?
这里就得说说库的链接顺序,但我们的musl库是和内核一起编译的,因此肯定会先链接我们自己的musl库,而后如果musl库里面没有该接口,才回去gcc内置库里面去寻找。
而且我们在链接阶段使用了:-nostdlib
,这会让我们告诉编译器不要链接标准库(如C库、数学库等)。
fno-builtin用法
那么问题来了,为什么gcc编译strlen调用的时候,既没有用内建的,也没有用我定义的,而是直接给出了长度?
如果您发现GCC在编译时没有调用内建的strlen
函数,也没有使用您定义的strlen
实现,而是直接给出了长度,这可能是由于编译器优化导致的。GCC有一个内置的strlen
函数,但在某些情况下,编译器可能会执行一种称为常量折叠的优化,尤其是在它能够确定字符串的长度在编译时是已知的情况下。
那么如何禁用内置函数的优化呢?
编译器特定选项: 使用编译器特定的选项来禁用特定类型的优化。例如,在GCC中,您可以使用-fno-builtin
来禁用内置函数的优化。
-fno-builtin
是GCC编译器的一个选项,用于控制编译器对内置函数的处理方式。在C语言中,有些函数是作为编译器内置的,这意味着编译器会直接将它们替换为相应的机器代码,而不是生成函数调用的代码。这通常用于性能优化,因为内置函数的调用开销更小,而且编译器可以针对具体的调用情况进行优化。
当您使用-fno-builtin
选项时,您是在告诉GCC不要将这些内置函数当作内置函数处理,而是按照普通的函数调用方式来编译它们。这意味着编译器会生成函数调用的代码,而不是直接替换为机器代码。
内置函数替换
关于内置函数,编译器会直接将它们替换为相应的机器代码,编译器是怎么知道的呢
编译器知道内置函数的实现并将其替换为机器代码,是因为这些函数的实现是编译器的一部分,而不是从外部库中链接进来的。这些内置函数通常包括C语言标准库中的函数(如printf
、scanf
、strlen
等),以及编译器提供的其他常用函数(如数学函数、字符串处理函数等)。
当编译器解析源代码时,它会识别出这些内置函数的调用。编译器内部有一个预定义的内置函数列表,这些函数的实现通常位于编译器的libgcc
或libstdc++
库中。当编译器遇到一个内置函数调用时,它会将这个调用替换为对应的机器代码,而不是生成一个函数调用。
例如,当编译器遇到以下C代码时:
printf("Hello, World!");
它会将printf
调用替换为libgcc
中printf
函数的机器代码,而不是生成一个函数调用。这使得调用更加快速,因为不需要跳转到外部函数的地址,也不需要执行额外的指令来执行函数调用。
内置函数的替换通常发生在编译器的前端,也就是在编译器的词法分析器(lexer)、语法分析器(parser)和语义分析器(semantic analyzer)阶段。在这些阶段,编译器解析源代码,识别出函数调用,并决定是否将其替换为内置函数的实现。
请注意,内置函数的替换是编译器优化的一部分,并不是所有编译器都会自动执行。在某些情况下,特别是当您使用特定的编译器选项(如-fno-builtin
)时,编译器可能不会进行内置函数的替换。此外,内置函数的替换也受到编译器配置和目标平台的影响。
O0取消替换?
-O0可以取消内置函数的替换吗
不,-O0
选项不能取消内置函数的替换。-O0
是GCC的优化级别选项,它表示不进行任何优化。然而,内置函数的替换是编译器默认的行为,即使在-O0
优化级别下,内置函数仍然会被替换为机器代码。
内置函数的替换是编译器实现的一部分,旨在提供高性能的调用,而不需要额外的函数调用开销。这种优化是编译器自动进行的,不受优化级别的影响。
如果您想要禁用内置函数的替换,您需要使用特定的编译器选项。例如,使用-fno-builtin
选项可以告诉GCC不要将某些函数作为内置函数处理。这个选项可以应用于特定的函数,也可以应用于所有的内置函数。
例如,如果您想要禁用对printf
函数的内置处理,可以使用以下命令:
gcc -fno-builtin-printf your_file.c -o your_program
这将告诉GCC不要将printf
函数作为内置函数处理,而是生成一个函数调用。请注意,这可能会影响性能,因为函数调用通常比内置函数的替换慢。