670 likes | 804 Views
使用手动和自动编译插桩对 CPU 运行时占用率突增进行检测. Adisak Pochanayon 首席软件工程师 Netherrealm 工作室 adisak@wbgames.com. 涵盖的主题. 本演讲是关于基于插桩的运行时间分析 。 代码插桩的方法 《 真人快打 》 ( MK )的 PET 分析程序( 占用率突增 检测器). 分析程序. 分析程序的 常见类型 硬件跟踪 基于事件的硬件 基于事件的软件 采样 插桩. 手动插桩. 显式插桩 / 代码标记 Wrapper 函数 Detours 代码库和“蹦床”( Trampolines )功能.
E N D
使用手动和自动编译插桩对CPU运行时占用率突增进行检测使用手动和自动编译插桩对CPU运行时占用率突增进行检测 Adisak Pochanayon 首席软件工程师 Netherrealm工作室 adisak@wbgames.com
涵盖的主题 • 本演讲是关于基于插桩的运行时间分析。 • 代码插桩的方法 • 《真人快打》(MK)的PET分析程序(占用率突增检测器)
分析程序 • 分析程序的常见类型 • 硬件跟踪 • 基于事件的硬件 • 基于事件的软件 • 采样 • 插桩
手动插桩 • 显式插桩 / 代码标记 • Wrapper函数 • Detours代码库和“蹦床”(Trampolines)功能
显式插桩 • 要求代码标记(修改源代码) • StartMarker(INFO) / StopMarker(INFO) • 作用域 - ScopedMarker(INFO) class CScopedMarker { CScopedMarker(ProfDesc &info) { StartMarker(info); } ~CScopedMarker(ProfDesc &info) { StopMarker(info); } }; #define ScopedMarker(INFO) CScopedMarker(INFO) \ ProfInfo##__LINE__
Wrapper函数 • 编译时间 • #定义函数 (…) wrapper函数 • 附加说明 – 与实现编译器无关 • 缺点– 只有在你有源代码时起作用 • 链接时间更换 / Wrapping • GCC选项:-Wl,--wrap,函数名 __wrap_函数名 () __real_函数名()
Wrapper函数 调用函数 目标函数
Wrapper函数 1 2 调用函数 “WRAPPER”函数 “REAL”函数 3 4
Wrapper函数 • 使用GCC / SNC进行wrapping malloc() 的示例 • 添加链接器标志:-Wl,--wrap,malloc extern "C" void* __real_malloc(size_t); extern "C" void* __wrap_malloc(size_t Size) { // 调用原始malloc() 函数 return __real_malloc(Size); }
Detours代码库和蹦床(Trampolines) • 这是一个为插桩修改代码的方法 • 可以由分析程序在目标代码/二进制文件上进行 • 调用库函数插桩的运行时间 • 请参阅微软Detours代码库 • MIPS示例代码(讲义) • 这是另一种形式的手动插桩,但此方式不要求对目标函数进行源代码标记。
Detours代码库和蹦床(Trampolines) 调用函数 目标函数
Detours代码库 1 2 调用函数 DETOUR函数 目标函数 跳转 3
蹦床(Trampolines)功能 Trampoline 缓冲器 调用函数 目标函数 目标PROLOG语言 目标PROLOG语言 拷贝
蹦床(Trampolines)功能 Trampoline 缓冲器 调动函数 目标函数 目标 PROLOG语言 目标 PROLOG语言 跳转
蹦床(Trampolines)功能 Trampoline 缓冲器 调用函数 目标函数 目标PROLOG语言 目标PROLOG语言 跳转
Detours代码库和蹦床(Trampolines) 1 Trampoline 缓冲器 2 3 调用函数 DETOUR函数 目标函数 跳转 目标 PROLOG语言 跳转 4 5 6
Detours代码库和蹦床(Trampolines) • 小结(缺点) • 必须自行编写:基于精简指令集RISC很琐碎 / 基于复杂指令集CISC更加困难 • 处理页面保护/ NX (不执行) • 商业使用需要付费 • 微软Detours软件 • http://research.microsoft.com/en-us/projects/detours/ • 微软1999年关于Detours代码库和蹦床功能(Trampolines)的研究论文: • http://research.microsoft.com/pubs/68568/huntusenixnt99.pdf
手动插桩 • 手动插桩方法总结 • 显示标记 • Wrapper函数 • Detours代码库和蹦床功能(Trampolines) • 所有方法都要求对函数进行识别和用户干预(代码标志、库函数调用或者链接器参数)。
自动插桩 • 你可能已经在使用自动插桩 • 有许多分析器支持自动插桩 • Metrowerks CATS、VTune Call Graph、Visual Studio Profiler & Visual C++ /callcap以及fastcap、GNU gprof (w/ gcc –pg) • 辅助编译器插桩(CAI) • 允许用户执行分析程序而编译器会为你进行标记 • GCC: -finstrument-functions / SNC: -Xhooktrace • Visual C++: _penter() & _pexit() using /Gh and /GH
自动插桩 函数体 PROLOG语言 当一个编译器为一个函数生成机器代码,除了函数体之外,它还生成一段prolog语言(保存寄存器、堆栈帧等等)以及epilog语言(恢复之前保存的寄存器和状态返回)。 EPILOG语言
自动插桩 编译器自动插桩 函数体 PROLOG语言 Log Entry { _penter() __cyg_profile_func_enter () } Log Exit { _pexit() __cyg_profile_func_exit () } EPILOG语言
GCC编译器 & SNC CAI自动编译器插桩 • 编译器选项:函式追踪 -finstrument-functions • 一般而言插桩需要进入和退出函数。在函数进入之后以及函数退出之前,使用当前函数及它的调用地址调用下列分析函数。 void __cyg_profile_func_enter (void *this_fn, void *call_site); void __cyg_profile_func_exit (void *this_fn,void *call_site);
SNC CAI自动编译器插桩 (PS3 / VITA) void __cyg_profile_func_enter(void *this_fn, void *call_site) { 如果(0==tls_PET_bIsInProcessing) { tls_PET_bIsInProcessing=true; _internal_PET_LogEntry(0); tls_PET_bIsInProcessing=false; } }
Visual C++ CAI • 在Visual C++里使用_penter() & _pexit()要稍微困难一些。 • Visual C++在函数入口和出口插入调用但并不保存任何寄存器。 • 最起码需要根据应用程序二进制接口ABI平台编写汇编程序来保存寄存器 • 需要额外的检查来确保“可用”
Visual C++ CAI – X86 extern "C" void __declspec(naked) _cdecl _penter( void ) { _asm { push eax push ebx push ecx push edx push ebp push edi push esi } if(0==tls_PET_bIsInProcessing) { tls_PET_bIsInProcessing=true; // Call C Work Function _internal_PET_LogEntry(0); tls_PET_bIsInProcessing=false; } _asm { pop esi pop edi pop ebp pop edx pop ecx pop ebx pop eax ret } }
Visual C++ CAI – XBOX 360 • 自动编译器插桩CAI支持XBOX 360平台(PowerPC) • 几乎和PC上一样 • 保存寄存器 • 新步骤 – 检查DPC(延迟过程调用) • TLS – 可重入检查 • 调用实际工作函数 • 恢复寄存器
Visual C++ CAI – XBOX 360 • PowerPC版本更加复杂 • 需要保存和恢复更多的应用程序二进制接口ABI寄存器 • 如果进行函数式程序设计FP,必须保存和恢复FP寄存器 • 优化和早期退出 • TLS访问必须在ASM中进行 • 混合了naked函数的asm / C语言不像在 X86上那样运行良好 • 后续内容请查看讲义…
Visual C++ CAI – XBOX 360 void __declspec(naked) _cdecl __penter( void ) { __asm { // Tiny Prolog // - 设置链接寄存器(r12) & 返回地址(两个步骤) std r12,-20h(r1) // 在此处保存链接寄存器LR是额外步骤! mflr r12 stw r12,-8h(r1) // 返回地址 blPET_prolog bl _internal_PET_LogEntry b PET_epilog } }
XBOX 360 CAI流程: _penter() 1 “C++” 插桩函数 _penter() {asm}
XBOX 360 CAI流程: _penter() 1 2 “C++” 插桩函数 _penter() {asm} PET_Prolog {asm} 3
XBOX 360 CAI流程: _penter() 4 1 2 3 “C++” 插桩函数 _penter() {asm} PET_Prolog {asm} PET_Prolog 早期退出 {asm} 3
XBOX 360 CAI流程: _penter() 1 2 “C++” 插桩函数 _penter() {asm} PET_Prolog {asm} 3 4 对进入Logging函数的“C++”分析例程 5
XBOX 360 CAI流程: _penter() 1 2 “C++” 插桩函数 _penter() {asm} PET_Prolog {asm} 3 4 对进入Logging函数的“C++”分析例程 5 6 PET_Epilog {asm} 7
XBOX 360 CAI流程: _penter() 4 1 2 3 “C++” 插桩函数 _penter() {asm} PET_Prolog {asm} PET_Prolog 早期退出{asm} 3 4 对进入Logging函数的“C++”分析例程 5 6 PET_Epilog {asm} 7
Visual C++ CAI – XBOX 360 • PET_Prolog语言具有五个特点 • 小型Prolog程序用来保存最小的寄存器 • 检查DPC & 可能的早期退出 • 检查递归 (TLS var) &可能的早期退出 • 保存临时对象(包括r2)并返回上级 • 早期退出一路返回至祖父类函数
Visual C++ CAI – XBOX 360 • 小型Prolog用来保存寄存器 // 小型Prolog // - 保存寄存器(r11,r14) // - 设置堆栈帧(r1) std r11,-30h(r1) std r14,-28h(r1) // 原来的堆栈指针 (r1) 在此指令后处于0(r1) stwu r1,-100h(r1)
Visual C++ CAI – XBOX 360 • 检查DPC & 可能的早期退出 // 获取基于特定线程的TLS lwz r11,0(r13) // 不要试图在目的信令点编码DPC中运行! // 在DPC中 { 0(r13) == 0 } cmplwi cr6,r11,0 beq cr6,label__early_exit_prolog
Visual C++ CAI – XBOX 360 • 检查递归 (TLS var) &可能的早期退出 lau r14,_tls_start // 获取基于全局的TLS lau r12,tls_PET_bIsInProcessing lal r14,r14,_tls_start lal r12,r12,tls_PET_bIsInProcessing sub r11,r11,r14 // TLS Base Offset (r11) add r14,r11,r12 // r14 == &tls_PET_bIsInProcessing // 使用变量线程 tls_PET_bIsInProcessing避免递归 lwzx r12,r11,r12 cmplwi cr6,r12,0 bne cr6,label__early_exit_prolog li r12,1 stw r12,0(r14) // 设置tls_PET_bIsInProcessing
Visual C++ CAI – XBOX 360 • 检查递归 (TLS var) &可能的早期退出 这看起来很复杂实际上非常简单。前一张幻灯片的内容等价于下列C++内容: 如果(tls_PET_bIsInProcessing) gotolabel__early_exit_prolog; tls_PET_bIsInProcessing=true;
Visual C++ CAI – XBOX 360 • 早期退出一路返回至祖父类函数 // 保存 r0/r2-r10 (临时对象) std r0,8h(r1) std r2,10h(r1) // (r2保存在 XBOX 360上) std r3,18h(r1) std r4,20h(r1) std r5,28h(r1) std r6,30h(r1) std r7,38h(r1) std r8,40h(r1) std r9,48h(r1) std r10,50h(r1) blr // 返回至调用者
Visual C++ CAI – XBOX 360 • 早期退出一路返回至祖父类函数 label__early_exit_prolog: // 小型Epilog – 调整堆栈 (r1) & 恢复 r12/r14/r11 addi r1,r1,100h lwz r12,-8h(r1) mtlr r12 ld r12,-20h(r1) ld r14,-28h(r1) ld r11,-30h(r1) blr
Visual C++ CAI – XBOX 360 • PET_Epilog更加简单(请参阅讲义) • 清除TLS递归预防变量 • 恢复临时对象 • 恢复用于小型Prolog中的寄存器 • 最后请注意:Worker函数如果执行任何浮点工作必须保存/恢复FP寄存器(fr0-fr13)。
XBOX 360 CAI流程: _pexit() 4 2 3 “C++” 插桩函数 _pexit() {asm} PET_Prolog {asm} PET_Prolog 早期退出{asm} 3 4 对进入Logging函数的“C++”分析例程 5 1 6 PET_Epilog {asm} 7
如何使用插桩 • 那么现在我们有了所有这些方法来进行代码插桩,我们该如何使用这些技术? • 将其挂接到你的分析代码中 • 在真人快打《Mortal Kombat》团队中,插桩运用的例子之一就是用于内部开发来检测运行时占用率突增的探测器,我们将之称为PET分析程序。
PET分析程序 • PET = Prolog Epilog Timing • 计算进入和退出函数的次数和检测峰值 • 能够设置一个全局阈值,任何超过该阈值的插桩函数都会受到记录 • 使用堆栈以及来自标识的潜在额外信息 • 运用自动编译器插桩进行工作 • 对游戏中每一个编译函数进行峰值检测 • 使用自动编译插桩CAI的费用大约为性能开销的15-30%
PET分析程序 • 没有代码标记需要检测峰值 • 打开CAI,它会自动找到执行时间超出你所设定的全局阈值的任何函数 • PET标签 / 代码标记仍然是有用的 • 简单的 PET_FUNCTION() (作用域标记) • 为PET分析程序提供额外的信息 • 允许在没有自动编译器插桩CAI的情况下在各平台上进行峰值检测 • 备用执行方案= 增强其他分析程序
PET的实现 • PET是通过使用TLS信息栈来实现 • 栈由CAI进入递增 • 栈由CAI退出递减 • 当CAI不存在时,PET栈的递增和递减由标记决定(作用域标记) • 作用域标记和应用程序编程接口API标记在全局级别、函数级别、子系统级别或者线程级别提供了额外的信息和功能
PET的实现 • 栈区数据 • 可以小至4个字,这取决于所选的定义#define选项 • 进入时间 • 可选阈值(覆盖全局阈值) • 针对子类对象的可选阈值 • 描述(其中大部分是可选的) • 函数地址 • 函数名称(指针为静态) • 线数 • 源文件名称(指针为静态) • 用户生成描述(动态字符串)
PET应用程序编程接口API / 基本标记 PET_FUNCTION() PET_SECTION_MANUAL(name) PET_SetGlobalThresholdMSec(msecs) PET_SetFunctionThresholdMSec(msecs) PET_SetChildrenThresholdMSec(msecs) PET_SetAllParentsThresholdMSec(msecs)
PET应用程序编程接口API / 阈值 为什么阈值是有效的?让我们假定你设置了一个全局阈值为1毫秒用来找到任何运行1毫秒以上的函数。对于一个60帧下运行的游戏,你的Game::Tick()函数将需要16.66毫秒的时间,这样可以避免触发峰值警告,你可以把下面这个标记插入到Game::Tick()中: PET_FUNCTION(); PET_SetFunctionThresholdMSec(1000.0/MIN_FPS); 那么PET分析程序将不会在你的Game::Tick()里记录一次峰值。