【问题标题】:How much stack and heap (in bytes) is required by the C function in X86X86 中的 C 函数需要多少堆栈和堆(以字节为单位)
【发布时间】:2019-01-02 06:11:17
【问题描述】:

“C”中函数sum的实现如下:

int sum(int b[], int c)
{
    int s,i;

    if (c<0)
    {
            printf("ERROR\n");
    }
    s = 0;

    for(i=0; i<c; ++i)
    {
        s = s + b[i];
    }
    return s;
}

我想知道,X86 Linux 平台的 sum 函数需要多少字节的栈和堆?这怎么知道?

从中断处理程序中调用函数是否可能有问题或成功?

【问题讨论】:

  • printf 不能从中断处理程序中运行;当中断触发时,您可能已经处于printf 的中间。除此之外,显然没有堆内存,因为没有动态分配。堆栈的使用取决于编译器,但应该在返回地址之外大约为零;一切都很容易放入寄存器中。编译它并查看编译器输出以查看它的作用,例如在编译器资源管理器上:godbolt.org
  • @duong_dajgja:这有点用,但是它们在编译时禁用了优化,所以没有任何东西会被优化掉。与此处不同的是,此函数的 -O0 版本将使用本地堆栈空间。
  • 您将其标记为 linux 设备驱动程序。您是从中断处理程序中运行它吗? printf 在这种情况下很糟糕。 printktrace_printk 可能是您想要查看的替代方案,如果它在内核上下文中运行。

标签: c linux linux-kernel x86 linux-device-driver


【解决方案1】:

在其他用户已经指出的基础上,我将尝试解决 OP 的两个问题。

OP的第一个问题:

我想知道,需要多少字节的堆栈和堆 X86 Linux平台中的函数sum?这怎么知道?

我们可以将第一个问题分成两部分。一个是关于堆栈大小,另一个是关于堆大小。

堆栈大小:

要了解您的函数使用了多少堆栈,您可以使用 GCC 诊断编译指示之一,即 -Wframe-larger-than=&lt;X&gt; 编译指示。这是一个示例,说明如何使用它。首先,我们将 pragma 添加到代码中并保存文件。

main.cpp

#include <stdio.h>
#pragma GCC diagnostic error "-Wframe-larger-than=1"

int sum(int b[], int c) {
    int s,i;
    if (c<0) {
            printf("ERROR\n");
    }
    s = 0;
    for(i=0; i<c; ++i) {
        s = s + b[i];
    }
    return s;
}

我们现在可以尝试编译代码:

junglefox@ubuntu:~$ gcc -c main.cpp
main.cpp: In function ‘int sum(int*, int)’:
main.cpp:20:1: error: the frame size of 32 bytes is larger than 1 bytes [-Werror=frame-larger-than=]
 }
 ^
cc1plus: some warnings being treated as errors
junglefox@ubuntu:~$

报告大小为 32 字节

  • 另一种测量堆栈大小的方法是使用 GCC 中的stack-usage 编译器标志。所以,我们删除或注释掉// #pragma GCC diagnostic error "-Wframe-larger-than=1"这一行,并再次尝试编译该文件,如下所示。

junglefox@ubuntu:~$ gcc -c main.cpp -fstack-usage

这将生成一个文件main.su

junglefox@ubuntu:~$ cat main.su 
main.cpp:5:5:int sum(int*, int) 48  static

这显然表明,我们正在使用 48 字节 的堆栈。


堆大小

要了解我们的程序使用了多少堆大小,我们将使用valgrind 工具Massif。为此,我们首先需要在代码中添加一个 main() 函数(没有它我们无法创建二进制文件。而二进制文件是我们需要使用 valgrind 运行的)。所以main.cpp,现在是这个样子,

#include <stdio.h>
// #pragma GCC diagnostic error "-Wframe-larger-than=1"

int sum(int b[], int c) {
    int s,i;
    if (c<0) {
            printf("ERROR\n");
    }
    s = 0;
    for(i=0; i<c; ++i) {
        s = s + b[i];
    }
    return s;
}

int main() {
    // As Peter pointed, uncomment one of the following lines,
    // for it to be a valid test. Also, compiler optimizations,
    // when turned on, can give different results.
    // sum(NULL,0);
    // sum(NULL,-1);
    return 0;
}

现在我们将在 valgrind 的帮助下编译、构建和运行二进制文件,如下所示:

junglefox@ubuntu:~$ gcc -o main main.cpp
junglefox@ubuntu:~$ valgrind ./main --tool=massif

这将生成一堆信息,如下所示:

    ==8179== Memcheck, a memory error detector
==8179== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==8179== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==8179== Command: ./main --tool=massif
==8179== 
==8179== 
==8179== HEAP SUMMARY:
==8179==     in use at exit: 0 bytes in 0 blocks
==8179==   total heap usage: 0 allocs, 0 frees, 0 bytes allocated
==8179== 
==8179== All heap blocks were freed -- no leaks are possible
==8179== 
==8179== For counts of detected and suppressed errors, rerun with: -v
==8179== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

报告总堆使用量为 0 千字节。

此外,正如@mevets 试图解释的那样,您始终可以查看编译器生成的底层汇编代码。在 GCC 中,您可以这样做,

junglefox@ubuntu:~/gcc -S main.cpp
junglefox@ubuntu:~/cat main.s

这将向您展示您的函数在底层程序集输出中的样子。

注意/编辑:但为了完整起见,在 C 或 C++ 中,如果没有使用 malloc()new 进行动态内存分配,作为程序员,您不会使用堆。此外,除非您在函数中声明一个数组,否则您不会使用任何大量的堆栈。


OP的第二个问题:

是否可能从中断处理程序中调用函数 有问题还是成功?

正如许多人在 cmets 中指出的那样,不要在 中断处理程序中使用 printf()

引用此link

中断处理程序与其他内核函数的区别 是内核调用它们以响应中断并且 它们在称为中断上下文的特殊上下文中运行。这个特别 上下文有时称为原子上下文,因为代码正在执行 在这种情况下无法阻止。

因为中断随时可能发生,所以中断处理程序可以 随时执行。处理程序必须运行 快速恢复中断代码的执行 可能。

因此,除了printf() 之外,可能需要很长时间的一件事是,当用作Interrupt Service Routine 时,您传递给该函数的数组有多大。它的复杂度为O(n)。如果c 太大,您的程序将暂停相对较长的时间,直到 ISR 完成该 for() 循环。

【讨论】:

  • 堆栈使用的好技巧。您忘记启用优化,这应该将此函数的优化降低到接近 0,具体取决于编译器围绕 printf 执行的操作。但是您的堆使用测试程序永远不会调用您要测试的函数。在这种情况下,为了让它可能使用任何堆,显然我们必须向它传递无效输入,以便它调用 printf。但是您确实需要将其作为程序执行的一部分来调用它才能成为有效的测试。
  • 其他使用堆栈的方式:深度嵌套的函数调用,尤其是通过具有中型到大型struct 本地变量的函数。这就是为什么 Linux 的 XFS 文件系统代码通常是内核堆栈问题的罪魁祸首。显然,未优化的递归可能会使用大量堆栈,所以也不要这样做。
  • @PeterCordes,是的,当然。递归。谢谢你指出这一点。写的时候,我并没有想到这一点。是的,具有大量变量的大型结构确实也会消耗“大量”堆栈。也感谢您提出这个问题。
  • @PeterCordes,奇怪!在向main() 函数添加一行sum(NULL, 0); 之后,我再次使用valgrind 编译并运行了代码。这次堆分配报告为0。这次我也关闭了优化。我删除了该行,再次编译并使用valgrind 再次运行,我再次看到堆使用情况为0。要么是我之前看到的东西,要么编译器在这里做了一些魔术,因为早些时候它报告了大约 84KB 的堆使用量。作为最终测试,我在main() 函数中使用了这一行sum(NULL, -1);,现在堆使用量精确到1KB - 因为printf()
  • 我希望 glibc 初始化函数的堆使用量不为零,至少达到峰值。大部分在退出前被释放也就不足为奇了。即使您不包含stdio.h 或包含对printf 的引用的链接代码,glibc 仍然会初始化一些表和缓冲区。
【解决方案2】:

多少堆栈?去掉printf后,需要0字节的栈;该函数很简单,可以使用 3 个寄存器:

.globl _sum
_sum: /* (int *b, int c); */
    mov 4(%esp), %edx
    mov 8(%esp), %ecx
    cmp $0, %ecx
    jl   badcount
    leal (%edx,%ecx,4), %ecx
    xor %eax, %eax
nxt:
    cmp %ecx, %edx
    je    done
    add (%edx), %eax
    add $4, %edx
    jmp nxt
done:
    ret
badcount:
    mov $-1, %eax
    ret

您需要能够依靠编译器来不做完全愚蠢的事情(无论标准 c## 怎么想)。

如果您已经走到了需要计算堆栈字节数的角落,请在您的路径中更早地查找错误。甚至 Frank n Furter 也意识到解决症状与解决原因是不同的。

【讨论】:

    【解决方案3】:

    C 没有“堆栈”的概念;当用 C 编写的代码被编译时,你应该认为它被破坏成无法识别的形式。

    例如,如果调用者这样做:

        myArray[2] = 99;
        result = sum( myArray, 4);
    

    然后编译器可以内联 sum() 函数的完全独立副本,然后(使用常量折叠、死代码消除和循环展开等优化)将该函数的单独副本转换为(等效于):

        result = myArray[0] + myArray[1] + 99 + myArray[3];
    

    ..然后对其进行注释(或转换为“单个静态赋值”形式)以允许更多并行性,例如:

        temp1 = (myArray[0] + myArray[1]); temp2 = (99 + myArray[3]);
        result = temp1 + temp2;
    

    ..然后转换成类似的东西:

        mov eax,[myArray]
        mov ebx,[myArray+4]
        lea eax,[eax+ebx]
    
        mov ecx,99
        mov edx,[myArray+12]
        lea ecx,[ecx+edx]
    
        lea eax,[eax+ecx]
    
        mov [result],eax
    

    ..然后优化和重新排序指令得到:

        mov eax,[myArray]
        mov ebx,[myArray+4]
        mov edx,[myArray+12]
        lea eax,[eax+ebx]
        lea eax,[eax+edx+99]
        mov [result],eax
    

    请注意,这看起来与原始代码完全不同 - 例如没有printf(),没有循环也没有分支。

    当然(假设函数不是static,并且假设没有进行链接时间优化或链接时间代码生成)编译器可能还会生成“我对调用者一无所知”的版本函数并将其推入输出目标文件,以防链接器需要它;并且(如果没有其他目标文件使用该函数)链接器可能会丢弃该版本的函数。在这种情况下;您可以查看编译器为“我对调用者一无所知”版本生成的代码,然后根据被丢弃且从未执行的代码使用的堆栈数量做出毫无价值/不正确的假设。

    【讨论】:

      猜你喜欢
      • 2021-03-27
      • 2015-04-15
      • 2015-08-21
      • 2013-06-18
      • 2015-09-03
      • 2019-01-05
      • 2012-12-02
      相关资源
      最近更新 更多