【问题标题】:Getting The Size of a C++ Function获取 C++ 函数的大小
【发布时间】:2011-04-13 21:05:26
【问题描述】:

我正在阅读this 问题,因为我试图在 C++ 程序中查找函数的大小,这暗示可能存在一种特定于平台的方法。我的目标平台是 windows

我目前脑海中的方法如下:
1.获取函数指针
2. 增加指针(& 计数器)直到达到ret 的机器码值
3.计数器的大小会有多大的功能?

Edit1:为了澄清“大小”的含义,我指的是构成函数的字节数(机器代码)。
Edit2:有几个 cmets 询问我为什么或打算如何处理这个问题。诚实的答案是我无意,而且我看不到知道函数长度预编译时间的好处。 (虽然我确定有一些)

这对我来说似乎是一个有效的方法,这可行吗?

【问题讨论】:

  • 不确定但如果一个函数有多个返回。这是否意味着你得到了有缺陷的结果?
  • 如果ret不止一个怎么办?如果函数是内联的呢?如果函数有已内联的子函数怎么办?
  • 坏主意。你到底想归档什么?
  • 很遗憾看到对一个可能只是在追求难以获得的计算机体系结构知识的问题有如此多的负面回答。我们应该给 OP 怀疑的好处。
  • @Michael:表明某事是个坏主意,但不理解其要点或目的(暗示这是一个坏主意后问“为什么这样做”),同时暗示它是无意义。这种评论的重点显然不是确定它是否是学术性的。假设好奇的东西一定是恶意的也不是很好。有很多方法可以问为什么有人想做某事。一个简单的例子是“你这样做是为了什么?”但这里的问题更长,表明这个问题没有用处。

标签: c++ windows code-size


【解决方案1】:

哇,我一直在使用函数大小计数,它有很多用途。它可靠吗?没门。它是标准的 C++ 吗?没门。但这就是为什么每次发布新版本时都需要在反汇编程序中检查它以确保它正常工作的原因。编译器标志可能会打乱顺序。

static void funcIwantToCount()
{
   // do stuff
}
static void funcToDelimitMyOtherFunc()
{
   __asm _emit 0xCC
   __asm _emit 0xCC
   __asm _emit 0xCC
   __asm _emit 0xCC
}

int getlength( void *funcaddress )
{
   int length = 0;
   for(length = 0; *((UINT32 *)(&((unsigned char *)funcaddress)[length])) != 0xCCCCCCCC; ++length);
   return length;
}

使用静态函数似乎效果更好。全局优化可以杀死它。

附:我讨厌人们,问你为什么要这样做,这是不可能的,等等。请不要再问这些问题了。让你听起来很愚蠢。程序员经常被要求做非标准的事情,因为新产品几乎总是会突破可用的极限。如果他们不这样做,那么您的产品可能是对已经完成的工作的重演。无聊!!!

【讨论】:

  • 嗨,Jordan,感谢您以这种聪明(而且有效!)的方式完成任务。我实际上在没有第二个静态函数的情况下成功了,只需将0xCCCCCC 直接放在主函数上即可。我不得不直接通过 ASM 手动清理堆栈。 __asm MOV ESP, EBP __asm POP EBP __asm RETN 此外,为了在没有占位符的情况下获得函数的确切长度,我将 ++length 更改为 length++
【解决方案2】:

不,这不起作用:

  1. 无法保证您的函数只包含一条 ret 指令。
  2. 即使它只包含一个 ret,您也不能只查看单个字节 - 因为相应的值可能只是一个值,而不是一条指令。

如果您将编码风格限制为,例如,在您的函数中只有一个返回点,则第一个问题可能会得到解决,但另一个基本上需要反汇编程序,以便您可以区分各个指令。

【讨论】:

  • 如果在程序的其他地方请求一个指向它的函数指针,该函数将永远不会被内联。不过,同意“这通常是不可能/困难的”概念。
  • 此外,没有什么说函数的编译输出需要是连续的。特别是在优化时,编译器(和链接器)会将代码块放置在各处(在函数之前,可能在另一个函数的中间),以尽量避免它认为是“热”路径的代码中的分支,将跳转目标放置在特定的对齐位置,或合并相同的指令序列(或其他)。关键是还有一件事让这件事变得愚蠢。
  • 按照迈克尔评论的意义,它甚至不需要包含ret 行。 GCC 有时会进行优化,它只是跳转到另一个已经包含 ret 的函数/块。我想如果它仅在异常情况下退出,它也可能没有返回。
  • 即使您将 C/C++ 编码风格修改为每个函数只有一个返回值,也不能保证编译器/优化器发出的汇编代码会执行相同的操作!
  • 除了已经提出的所有伟大论点之外,没有人提到,在可变宽度指令集 (如 x86) 中,它甚至是 不可能的在不运行代码的情况下可靠地反汇编代码。例如,考虑程序集jmp Cont; db 01; Cont: ...,即在jmp 语句和下一条语句之间插入字节0x01。当您逐步执行时,您无法知道0x01 不是 下一条指令的开始,因此在该点之后反汇编的所有内容都将是垃圾。这就是为什么为 x86 编写反汇编程序如此困难的原因。
【解决方案3】:

获取函数的所有块是可能的,但询问函数的“大小”是一个不自然的问题。优化后的代码将按照执行顺序重新排列代码块,并将很少使用的块(异常路径)移动到模块的外部。有关更多详细信息,请参阅Profile-Guided Optimizations,例如 Visual C++ 如何在链接时间代码生成中实现这一点。所以一个函数可以从地址 0x00001000 开始,在 0x00001100 处跳转到 0x20001000 处的跳转和 ret,并有一些异常处理代码 0x20001000。在 0x00001110 处启动另一个函数。您的功能的“大小”是多少?它的范围确实从 0x00001000 到 +0x20001000,但它在该范围内只“拥有”几个块。所以你的问题应该是不问的。

在这个上下文中还有其他有效的问题,比如一个函数的指令总数(可以从程序符号数据库和图像中确定),更重要的是,频繁执行的指令数是多少函数内部的代码路径。所有这些都是在性能测量的上下文中通常提出的问题,并且有一些工具可以检测代码并且可以给出非常详细的答案。

在内存中追逐指针并搜索ret 恐怕会让你无处可去。现代代码远比这复杂得多。

【讨论】:

  • 顺便说一句,这就是我查看函数“大小”的方法:在 Windbg 中加载模块,然后 uf module!functionameuf 在全功能反汇编方面做得很好,即使在处理现代生成/优化工具分解成碎片的代码时,我也可以看到指令、块等的数量。
【解决方案4】:

这行不通...如果有一个跳跃,一个假人ret,然后是跳跃的目标怎么办?你的代码会被愚弄。

一般来说,不可能做到 100% 准确,因为您必须预测所有代码路径,这就像解决 halting problem 一样。如果您实现自己的反汇编程序,您可以获得“相当不错”的准确性,但没有任何解决方案会像您想象的那样简单。

一个“技巧”是找出哪个函数的代码是您正在寻找的函数之后,这将给出相当好的结果假设某些(危险) 假设。但是你必须知道你的函数后面是什么函数,优化后很难弄清楚。


编辑 1:

如果函数甚至没有以ret 指令结束怎么办?它很可能只是将jmp 回复给它的调用者(尽管不太可能)。


编辑 2:

别忘了 x86 至少有可变长度指令...


更新:

对于那些说流量分析与解决停机问题不同的人:

考虑当你有这样的代码时会发生什么:

foo:
    ....
    jmp foo

每次都必须跟随跳转来找出函数的结束,而你不能在第一次之后忽略它,因为你不知道无论您是否正在处理自修改代码。 (例如,您可以在 C++ 代码中使用内联汇编来修改自身。)它可以很好地扩展到内存的其他位置,因此您的分析器将(或应该)以无限循环结束,除非您容忍误报。

这不就是停机问题吗?

【讨论】:

  • 有很多可能做到这一点。您不需要证明给定分支是否可访问——这是您解决停止问题所需要的。然而,枚举可能的代码路径是流分析,这是非常可行的。
  • 基本上你可以用计算圈复杂度的方法来计算。
  • @Billy:你需要证明某段代码是可达的,不是吗?这似乎与我的停机问题非常相似......
  • @Billy:对不起,你能再解释一下吗? “像处理器一样跟随代码”被称为运行程序。除了运行程序来确定一个函数的返回值是一种非常低效的想法之外,不能保证程序在运行期间甚至会调用该函数。而且你不能简单地从相关函数开始运行程序,因为你不知道函数期望什么样的输入值——不仅仅是函数参数,还有全局变量状态。
  • @Billy:但这就是我要说的——即使你知道函数只有一个返回点,如果jnz 跳转到一条指令的中间,你将有两个完全具有(可能)两个不同的ret 语句的不同代码路径。你怎么知道哪个是正确的ret(注意这并不总是“最远的ret”)?唯一知道的方法就是知道jnz会不会跳转,这是一个无法判断的问题。
【解决方案5】:

可以在非常有限的情况下工作。我在我编写的代码注入实用程序的一部分中使用它。我不记得我在哪里找到的信息,但我有以下(VS2005 中的 C++):

#pragma runtime_checks("", off)

static DWORD WINAPI InjectionProc(LPVOID lpvParameter)
{
    // do something
    return 0;
}

static DWORD WINAPI InjectionProcEnd()
{
    return 0;
}

#pragma runtime_checks("", on)

然后我还有其他一些功能:

size_t cbInjectionProc = (size_t)InjectionProcEnd - (size_t)InjectionProc;

您必须关闭一些优化并将函数声明为静态才能使其工作;我不记得具体的了。我不知道这是否是一个精确的字节数,但它足够接近。大小只是立即函数的大小;它不包括该函数可能调用的任何其他函数。除了像这样的极端情况之外,“函数的大小”是没有意义和无用的。

【讨论】:

  • 这里有两个更正和一个警告:1) 我会在您的方法声明中添加__declspec(noinline),以防止在release 构建中内联。 2) 它应该是#pragma runtime_checks("", restore) 而不是on 3) 警告: 如果您计划将此方法注入一个单独的进程,如果您的InjectionProc 方法有任何静态链接,这将导致它崩溃API 调用。
【解决方案6】:

我发这个是为了说两件事:

1)这里给出的大多数答案都非常糟糕,很容易崩溃。如果您使用 C 函数指针(使用函数名),在您的可执行文件的 debug 构建中,并且可能在其他情况下,它可能指向一个 JMP shim函数体本身。这是一个例子。如果我对下面定义的函数执行以下操作:

FARPROC pfn = (FARPROC)some_function_with_possibility_to_get_its_size_at_runtime;

我得到的pfn(例如:0x7FF724241893)会指向这个,这只是一个JMP指令:

此外,编译器可以嵌套多个垫片,或分支您的函数代码,使其具有多个epilogsret 指令。哎呀,它甚至可能不使用ret 指令。然后,无法保证函数本身会按照您在源代码中定义的顺序进行编译和链接。

你可以用 汇编 语言做所有这些事情,但不能用 C 或 C++。

2) 以上是坏消息。好消息是原始问题的答案是,是的,有一种方法(或 hack)可以获得确切的函数大小,但它附带以下限制:

  • 它仅适用于 Windows 上的 64 位可执行文件。

  • 它显然是 Microsoft 特定的,不可移植。

  • 您必须在运行时执行此操作。

这个概念很简单——利用 x64 Windows 二进制文件中的SEH is implemented 方式。编译器将每个函数的详细信息添加到 PE32+ 头文件中(添加到可选头文件的IMAGE_DIRECTORY_ENTRY_EXCEPTION 目录中),您可以使用它来获取确切的函数大小。 (如果您想知道,此信息用于在 __try/__except/__finally 块中捕获、处理和展开异常。)

这是一个简单的例子:

//You will have to call this when your app initializes and then
//cache the size somewhere in the global variable because it will not
//change after the executable image is built.

size_t fn_size; //Will receive function size in bytes, or 0 if error
some_function_with_possibility_to_get_its_size_at_runtime(&fn_size);

然后:

#include <Windows.h>

//The function itself has to be defined for two types of a call:
// 1) when you call it just to get its size, and
// 2) for its normal operation
bool some_function_with_possibility_to_get_its_size_at_runtime(size_t* p_getSizeOnly = NULL)
{
    //This input parameter will define what we want to do:
    if(!p_getSizeOnly)
    {
        //Do this function's normal work
        //...

        return true;
    }
    else
    {
        //Get this function size
        //INFO: Works only in 64-bit builds on Windows!
        size_t nFnSz = 0;

        //One of the reasons why we have to do this at run-time is
        //so that we can get the address of a byte inside 
        //the function body... we'll get it as this thread context:
        CONTEXT context = {0};
        RtlCaptureContext(&context);

        DWORD64 ImgBase = 0;
        RUNTIME_FUNCTION* pRTFn = RtlLookupFunctionEntry(context.Rip, &ImgBase, NULL);
        if(pRTFn)
        {
            nFnSz = pRTFn->EndAddress - pRTFn->BeginAddress;
        }

        *p_getSizeOnly = nFnSz;
        return false;
    }
}

【讨论】:

    【解决方案7】:

    解决这个问题的真正方法是深入研究编译器的文档。我们使用的 ARM 编译器可以生成一个程序集转储 (code.dis),从中减去给定的错位函数标签和下一个错位函数标签之间的偏移量是相当简单的。

    不过,我不确定您需要哪些工具来处理 Windows 目标。看起来this question 的答案中列出的工具可能就是您想要的。

    另外请注意,我(在嵌入式领域工作)假设您在谈论编译后分析。如果满足以下条件,仍然可以通过编程方式检查这些中间文件作为构建的一部分:

    • 目标函数在不同的对象中
    • 构建系统已学习依赖项
    • 您肯定知道编译器会构建这些目标文件

    请注意,我不完全确定您为什么想知道这些信息。过去我需要它来确保我可以将特定的代码块放在内存中非常特定的位置。我不得不承认我很好奇这对更通用的桌面操作系统目标有什么作用。

    【讨论】:

      【解决方案8】:

      在 C++ 中,没有函数大小的概念。除了提到的所有其他内容之外,预处理器宏也会产生不确定的大小。如果你想计算指令字的数量,你不能在 C++ 中这样做,因为它在编译之前是不存在的。

      【讨论】:

      • 这就是使用指针的意义所在。 (没有双关语)我的目标是获取构成内存中函数的字节数,即机器代码。
      【解决方案9】:

      “函数的大小”是什么意思?

      如果你指的是一个函数指针,那么对于 32 位系统来说,它总是只有 4 个字节。

      如果您指的是代码的大小,那么您应该只反汇编生成的代码并找到入口点和最近的ret 调用。一种方法是在函数的开头和结尾读取指令指针寄存器。

      如果您想计算函数在平均情况下调用的指令数,您可以使用分析器并将停用的指令数除以调用数。

      【讨论】:

        【解决方案10】:

        我认为它适用于使用 msvc 创建的 windows 程序,至于分支,'ret' 似乎总是出现在最后(即使有提前返回的分支,它也会结束)。 但是,您将需要某种反汇编程序库来计算当前操作码长度,因为它们是 x86 的可变长度。如果你不这样做,你会遇到误报。

        如果有些情况没有发现,我不会感到惊讶。

        【讨论】:

          【解决方案11】:

          标准 C++ 中没有获取函数大小或长度的工具。
          在这里查看我的答案:Is it possible to load a function into some allocated memory and run it from there?

          一般来说,在嵌入式系统中,将可执行代码从只读源(或慢速存储设备,如串行闪存)复制到 RAM 时,会使用知道函数的大小。桌面和其他操作系统使用其他技术(例如动态或共享库)将函数加载到内存中。

          【讨论】:

            【解决方案12】:

            只需在获得函数的地址设置 PAGE_EXECUTE_READWRITE 即可。然后读取每个字节。当你得到字节“0xCC”时,这意味着函数的结尾是actual_reading_address - 1。

            【讨论】:

              【解决方案13】:

              使用 GCC,一点也不难。

              void do_something(void) { 
                 printf("%s!", "Hello your name is Cemetech"); 
                 do_something_END: 
              } 
              
              ... 
              
                 printf("size of function do_something: %i", (int)(&&do_something_END - (int)do_something));
              

              【讨论】:

              • 这能保证准确吗?我不认为编译器保证了函数中指令的顺序。
              • 首先,您似乎需要在标签后添加一个声明,其次,如果您需要 &amp;&amp;do_something_END 在文件范围内可用,这将不起作用,使其或多或少无用。跨度>
              【解决方案14】:

              下面的代码获取准确的功能块大小,它适用于我的测试 runtime_checks 在调试模式下禁用 _RTC_CheckEsp

                  #pragma runtime_checks("", off)
              DWORD __stdcall loadDll(char* pDllFullPath)
              {  
                  OutputDebugStringA(pDllFullPath);
                  //OutputDebugStringA("loadDll...................\r\n");
                  return 0;
                  //return test(pDllFullPath);
              }
              #pragma runtime_checks("", restore)
              
              DWORD __stdcall getFuncSize_loadDll()
              {
                  DWORD maxSize=(PBYTE)getFuncSize_loadDll-(PBYTE)loadDll;
                  PBYTE pTail=(PBYTE)getFuncSize_loadDll-1;
                  while(*pTail != 0xC2 && *pTail != 0xC3) --pTail;
                  if (*pTail==0xC2)
                  {   //0xC3          : ret
                      //0xC2 04 00    : ret 4
                      pTail +=3;
                  }
              
                  return pTail-(PBYTE)loadDll;
              };
              

              【讨论】:

                【解决方案15】:

                不可移植但基于 API 且工作正常的方法是使用程序数据库读取器 - 例如 Windows 上的 dbghelp.dll 或 Linux 上的 readelf。仅当调试信息与程序一起启用/存在时,才能使用这些。这是一个关于它如何在 Windows 上工作的示例:

                SYMBOL_INFO symbol = { };
                
                symbol.SizeOfStruct = sizeof(SYMBOL_INFO);
                
                // Implies, that the module is loaded into _dbg_session_handle, see ::SymInitialize & ::SymLoadModule64
                ::SymFromAddr(_dbg_session_handle, address, 0, &symbol);
                

                您将在 symbol.Size 中获得函数的大小,但您可能还需要额外的逻辑来识别给定的地址实际上是一个函数、增量链接器放置在那里的垫片还是一个DLL 调用 thunk(同样的事情)。

                我想在 Linux 上可以通过 readelf 完成类似的操作,但也许您必须在其源代码之上提供该库...

                您必须记住,虽然基于反汇编的方法是可能的,但您基本上必须分析一个有向图,其端点为 ret、halt、jmp(前提是您启用了增量链接并且您能够读取 jmp -table 来确定您在函数中面临的 jmp 是该函数的内部(在图像的 jmp 表中缺失)还是外部(存在于该表中;此类 jmp 经常作为 x64 上尾调用优化的一部分发生,因为我知道)),任何意味着 nonret 的调用(例如生成异常的帮助器)等。

                【讨论】:

                  【解决方案16】:

                  这是一个老问题,但仍然......

                  对于 Windows x64,函数都有一个函数表,其中包含函数的偏移量和大小。 https://docs.microsoft.com/en-us/windows/win32/debug/pe-format 。该函数表用于在抛出异常时展开。

                  也就是说,这不包含像内联这样的信息,以及人们已经注意到的所有其他问题......

                  【讨论】:

                    【解决方案17】:
                    int GetFuncSizeX86(unsigned char* Func)
                    {
                        if (!Func)
                        {
                            printf("x86Helper : Function Ptr NULL\n");
                            return 0;
                        }
                    
                        for (int count = 0; ; count++)
                        {
                            if (Func[count] == 0xC3)
                            {
                                unsigned char prevInstruc = *(Func - 1);
                                if (Func[1] == 0xCC // int3
                                    || prevInstruc == 0x5D//  pop    ebp
                                    || prevInstruc == 0x5B//  pop    ebx
                                    || prevInstruc == 0x5E//  pop    esi
                                    || prevInstruc == 0x5F//  pop    edi
                                    || prevInstruc == 0xCC//  int3
                                    || prevInstruc == 0xC9)// leave
                                    return count++;
                            }
                        }
                    }
                    

                    你可以使用这个假设你在 x86 或 x86_64

                    【讨论】:

                    • 您好 Josua,欢迎您。这可能适用于您的代码(您测试过它吗?什么编译器?),但作为一般答案,它 very 依赖于编译器.. 我可以问很多“关于”的问题:中途退货怎么办?如果编译器行为在未来版本中发生变化,您必须在每次更新时进行修改和测试。如果先使用跳转完成返回怎么办..
                    • 我在visual studio的msvc上用过,还有clang编译器,你是对的,代码很简单!
                    • 在这里看看第三个答案。 Jordan 使用相同的方法(拿起程序集并扫描它),但 Jordan 的代码独立于平台并且独立于函数程序集中的 return (0xC3) 语句的位置。您不知道,程序集中是否有多个 ret (0xC3) 语句。它可能适用于您的代码,但不适用于所有 legal 函数。我认为这是您解决方案中最重要的问题。
                    • 你是完全正确的,下次我会在上传之前评估我的代码在其他问题的上下文中可以做什么!
                    猜你喜欢
                    • 2012-02-01
                    • 1970-01-01
                    • 2020-07-24
                    • 1970-01-01
                    • 2021-02-13
                    • 2011-10-19
                    • 2015-05-30
                    • 1970-01-01
                    相关资源
                    最近更新 更多