24

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

我目前脑海中的方法如下:
1. 获取指向函数
的指针 2. 递增指针(& 计数器)直到达到机器码值ret
3. 计数器将是函数的大小?

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

这对我来说似乎是一种有效的方法,这行得通吗?

4

17 回答 17

16

哇,我一直在使用函数大小计数,它有很多用途。它可靠吗?没门。它是标准的 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;
}

它似乎与静态函数一起工作得更好。全局优化可以杀死它。

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

于 2012-04-09T09:30:15.067 回答
15

不,这不起作用:

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

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

于 2011-04-13T21:12:52.297 回答
13

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

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

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

于 2011-04-13T21:23:23.700 回答
7

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

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

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


编辑1:

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


编辑2:

不要忘记,至少 x86 有可变长度指令......


更新:

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

考虑当您有如下代码时会发生什么:

foo:
    ....
    jmp foo

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

这不是像停机问题吗?

于 2011-04-13T21:08:42.673 回答
3

可以在非常有限的情况下工作。我在我编写的代码注入实用程序的一部分中使用它。我不记得我在哪里找到的信息,但我有以下(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;

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

于 2011-04-14T00:02:28.160 回答
3

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

1)这里给出的大多数答案都非常糟糕,很容易崩溃。如果您在debug可执行文件的构建中使用 C 函数指针(使用函数名),并且可能在其他情况下,它可能指向一个没有函数体本身的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 位可执行文件。

  • 它显然是微软特有的,不可移植。

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

这个概念很简单——利用在 x64 Windows 二进制文件中实现 SEH的方式。编译器将每个函数的详细信息添加到 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;
    }
}
于 2018-02-10T07:07:00.113 回答
2

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

但是,我不确定对于 Windows 目标您需要哪些工具。看起来这个问题的答案中列出的工具可能就是您正在寻找的工具。

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

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

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

于 2011-04-13T21:14:19.793 回答
1

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

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

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

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

于 2011-04-13T21:11:55.033 回答
1

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

于 2011-04-13T21:12:01.837 回答
1

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

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

于 2011-04-13T21:20:36.763 回答
1

标准 C++ 中没有获取函数大小或长度的工具。
在这里查看我的答案: 是否可以将函数加载到一些分配的内存中并从那里运行它?

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

于 2011-04-13T23:24:56.153 回答
1

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

于 2011-07-27T13:41:49.950 回答
0

使用 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));
于 2012-01-08T16:50:32.807 回答
0

下面的代码获得了准确的功能块大小,它可以在调试模式下与我的测试 runtime_checks disable _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;
};
于 2018-01-10T08:19:44.967 回答
0

不可移植但基于 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 的调用(如异常生成助手)等。

于 2018-11-01T18:22:50.297 回答
0

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

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

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

于 2019-10-12T15:10:47.457 回答
0
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

于 2021-08-20T17:04:23.460 回答