.Net CLR GC动态获取函数头地址,C++的骚操作(慎入)
前言:
太懒了,从没有在这里正儿八经的写过文章。看到一些人的高产,真是惭愧。决定稍微变得不那么懒。如有疏漏,请指正。
.net的GC都谈的很多了,本篇主要是剑走偏锋,聊聊一些个人认为较为核心的细节方面的问题。至于,标记,计划,压缩,清扫这些不在讨论之列。
动态函数头地址的一些概念:
一段内存有内存的起始地址(暂叫base),内存的结束地址,以及内存指针当前指向的地址大致的三个概念。而在这段内存里面分配了函数之后,一个函数在内存里面必定有一个函数的起始地址也就是指令(第一个push)所在的地址,称之为函数头地址,函数的结束地址也就是指令(ret)所在的地址。在函数里面做了一些事情,那么这些可以称之为函数中间的某个地址。
通过函数中间的某个地址(不固定的)获取到函数头地址(固定的)。称之为动态获取函数头地址
硬编码动态获取到函数头地址之后,你就可以得到GC信息,方法描述符信息,调试信息,异常信息,回滚信息,帧栈信息等等。
C#代码:
static void Main(string[] args)
{
GC.Collect();
Console.ReadLine();
}
把这段代码反汇编一下:
7: static void Main(string[] args)
8: {
00007FFB098C5EC0 55 push rbp
00007FFB098C5EC1 57 push rdi
00007FFB098C5EC2 56 push rsi
00007FFB098C5EC3 48 83 EC 30 sub rsp,30h
00007FFB098C5EC7 48 8B EC mov rbp,rsp
00007FFB098C5ECA 33 C0 xor eax,eax
00007FFB098C5ECC 48 89 45 28 mov qword ptr [rbp+28h],rax
00007FFB098C5ED0 48 89 4D 50 mov qword ptr [rbp+50h],rcx
00007FFB098C5ED4 83 3D 95 CB 09 00 00 cmp dword ptr [7FFB09962A70h],0
00007FFB098C5EDB 74 05 je ConsoleApp10.Program.Main(System.String[])+022h (07FFB098C5EE2h)
00007FFB098C5EDD E8 0E 27 CB 5F call 00007FFB695785F0
00007FFB098C5EE2 90 nop
9: GC.Collect();
00007FFB098C5EE3 E8 70 ED FF FF call CLRStub[MethodDescPrestub]@7ffb098c4c58 (07FFB098C4C58h)
00007FFB098C5EE8 90 nop
10: Console.ReadLine();
00007FFB098C5EE9 E8 42 FF FF FF call CLRStub[MethodDescPrestub]@7ffb098c5e30 (07FFB098C5E30h)
00007FFB098C5EEE 48 89 45 28 mov qword ptr [rbp+28h],rax
00007FFB098C5EF2 90 nop
11: }
00007FFB098C5EF3 90 nop
00007FFB098C5EF4 48 8D 65 30 lea rsp,[rbp+30h]
00007FFB098C5EF8 5E pop rsi
00007FFB098C5EF9 5F pop rdi
00007FFB098C5EFA 5D pop rbp
00007FFB098C5EFB C3 ret
我们看到地址:00007FFB098C5EC0就是函数头的地址。00007FFB098C5EFB则是函数结束地址。中间的比如调用GC.Collection的地址00007FFB098C5EE3和调用Console.ReadLine的地址00007FFB098C5EE9,则可以称之为中间地址。
如何通过中间的某个地址(可能是00007FFB098C5EE3,也可能是00007FFB098C5EE9,还有可能是中间其它地址)动态的找到函数头的固定地址呢?
计算公式一:奇偶数的偏移(value-1)
我们先来看下函数头地址:00007FFB098C5EC0,在内存里面的存储数值。
CLR的操作是:
value-1 =(00007FFB098C5EC0 - base) & 31 >>2+1
base:是函数所在内存的其实地址
value-1:是计算的结果
这个value-1的结果要么是1,要么是5,为啥?仔细分析下。一般的来说,base也就是函数所在的内存的其实地址末尾两字节一般都是 00 00。也就是说base - 00007FFB098C5EC0的结果一定四0xnnnnnnnnnnnn5EC0。n表示未知数。因为上面的公式&31,所以只需要关注最后两个字节就可以了。
回到上面为啥value-1等于1或者5呢?不能等于其它。5EC0中C0的二进制是:
1100 0000。把它&31,结果是0。0>>2还是0。然后加上1,结果也就是value-1等于1.
那么5是怎么来的呢?我们注意看,0xC是能被2整除的偶数。如果是不能被2整除的奇数,比如0xD的话,低位的向左第五位必定位1,其它第四位无论是什么,右移2之后一定是4,然后 4+1 等于5。
所以低位向左第五位如果是偶数,则value-1为1,如果是奇数则value-1为5。不能有其它,此处大家可以自行验证。
关于计算公式参考:https://github.com/dotnet/runtime/blob/main/src/coreclr/vm/codeman.cpp
计算公式二:0的个数的32位索引
标题头的意思是:以0的个数表示有几个32
还是按照上面来,此处函数头的其实地址是:00007FFB098C5EC0。这里的计算公式略有不同:
value-2 = 28 - (00007FFB098C5EC0 - base) >> 5 & 7 << 2
同样:
base:表示函数所在内存的起始地址
value-2 则是此公式计算的结果
因为此公式右移的是5,而且base最后两位一般为0。所以只需要看最后一字节也就是C0即可。
1100 0000 右移5位,结果为0110,也就是6。6&7等于6,6左移2,结果为0x18。十进制的24。然后28-24 ==4。value-2的结果为4。
公式一计算得出的value-1的值为1。因为C0的C是偶数。所以为1。
公式二计算得出的value-2的值为4。
value = value-1 << value-2
value就是最终函数头地址:00007FFB098C5EC0在内存里面存储的形式,二进制表示就是:0001 0000。十进制的:16 。十六进制的:0x10 。
关于计算公式参考:https://github.com/dotnet/runtime/blob/main/src/coreclr/vm/codeman.cpp
中间地址计算动态找出函数头:
此处中间地址取GC.Collection的地址:00007FFB098C5EE3。
startPos = (00007FFB098C5EE3 - base) >> 5,此处取GC.Collection地址的最后两位5EE3 >> 5。结果为:startPos = 0x2F7。
首先从内存里面取出公式二里面计算的value值:0x10。然后套用公式二的value-2的计算:
Result = 28 -(00007FFB098C5EE3 - base) >> 5 & 7 << 2
很明显Result的结果为 0
把tmp = value >> Result 。
结果tmp == 0x10。
if (tmp)
{
startPos--;
while (!(tmp & NIBBLE_MASK))
{
tmp = tmp >> NIBBLE_SIZE;
startPos--;
}
return base + POSOFF2ADDR(startPos, tmp & NIBBLE_MASK);
}
NIBBLE_MASK:0xf
POSOFF2ADDR: startPos << 5 + (tmp -1 ) << 2
因为tmp为0x10,所以startPos--。 2f7-1 == 2f6 。然后因为 !(tmp & NIBBLE_MASK) 所以 tmp = tmp >> NIBBLE_SIZE; 也就是 tmp == 1。
那么结果就是 base + 2f6 << 5 + (1 -1) << 2
用n表示未知数 0xnnnnnnnnnnnn5EC0。刚好是函数头的地址。
此方法适用于任何一个中间地址动态获取函数头地址。
过程
我们在C#源代码中调用GC.Collection会运行以下几个步骤:
1.GC.Collection()
2.GCScanRoot()
3.EECodeInfo.Init(寄存器Rip)
4.FindMethodCode(寄存器Rip)
5.通过FindMethodCode找到函数头地址,然后通过函数头的地址-8。得到的就是EHinfo,DebugInfo,GCinfo,MethodDesc,UwndInfo信息
6.通过GCinfo找到根对象
7.通过根对象遍历所有对象
8.在这些对象中找到非存活对象,然后进行回收
这个过程过于复杂,省略了很多与本节主题无关的东西。我们看到FindMethodCode就是获取到函数头的地址的函数。
公式一和二的参考如下:
公式一:
void EEJitManager::NibbleMapSetUnlocked(HeapList * pHp, TADDR pCode, BOOL bSet)
{
CONTRACTL {
NOTHROW;
GC_NOTRIGGER;
} CONTRACTL_END;
// Currently all callers to this method ensure EEJitManager::m_CodeHeapCritSec
// is held.
_ASSERTE(m_CodeHeapCritSec.OwnedByCurrentThread());
_ASSERTE(pCode >= pHp->mapBase);
size_t delta = pCode - pHp->mapBase;
size_t pos = ADDR2POS(delta);
DWORD value = bSet?ADDR2OFFS(delta):0;
DWORD index = (DWORD) (pos >> LOG2_NIBBLES_PER_DWORD);
DWORD mask = ~((DWORD) HIGHEST_NIBBLE_MASK >> ((pos & NIBBLES_PER_DWORD_MASK) << LOG2_NIBBLE_SIZE));
value = value << POS2SHIFTCOUNT(pos);
PTR_DWORD pMap = pHp->pHdrMap;
// assert that we don't overwrite an existing offset
// (it's a reset or it is empty)
_ASSERTE(!value || !((*(pMap+index))& ~mask));
// It is important for this update to be atomic. Synchronization would be required with FindMethodCode otherwise.
*(pMap+index) = ((*(pMap+index))&mask)|value;
}
公式二:
TADDR EEJitManager::FindMethodCode(RangeSection * pRangeSection, PCODE currentPC)
{
LIMITED_METHOD_DAC_CONTRACT;
_ASSERTE(pRangeSection != NULL);
HeapList *pHp = dac_cast<PTR_HeapList>(pRangeSection->pHeapListOrZapModule);
if ((currentPC < pHp->startAddress) ||
(currentPC > pHp->endAddress))
{
return NULL;
}
TADDR base = pHp->mapBase;
TADDR delta = currentPC - base;
PTR_DWORD pMap = pHp->pHdrMap;
PTR_DWORD pMapStart = pMap;
DWORD tmp;
size_t startPos = ADDR2POS(delta); // align to 32byte buckets
// ( == index into the array of nibbles)
DWORD offset = ADDR2OFFS(delta); // this is the offset inside the bucket + 1
_ASSERTE(offset == (offset & NIBBLE_MASK));
pMap += (startPos >> LOG2_NIBBLES_PER_DWORD); // points to the proper DWORD of the map
// get DWORD and shift down our nibble
PREFIX_ASSUME(pMap != NULL);
tmp = VolatileLoadWithoutBarrier<DWORD>(pMap) >> POS2SHIFTCOUNT(startPos);
if ((tmp & NIBBLE_MASK) && ((tmp & NIBBLE_MASK) <= offset) )
{
return base + POSOFF2ADDR(startPos, tmp & NIBBLE_MASK);
}
// Is there a header in the remainder of the DWORD ?
tmp = tmp >> NIBBLE_SIZE;
if (tmp)
{
startPos--;
while (!(tmp & NIBBLE_MASK))
{
tmp = tmp >> NIBBLE_SIZE;
startPos--;
}
return base + POSOFF2ADDR(startPos, tmp & NIBBLE_MASK);
}
}
你也可以直接参考:
https://github.com/dotnet/runtime/blob/main/src/coreclr/vm/codeman.cpp
微信公众号:jianghupt. QQ群:676817308
.Net CLR GC动态获取函数头地址,C++的骚操作(慎入)的更多相关文章
- C# — 动态获取本地IP地址及可用端口
1.在VS中动态获取本地IP地址,代码如下: 2.获取本机的可用端口以及已使用的端口:
- php中如何动态获取函数的参数
php动态获取函数参数 一.总结 一句话总结:a.PHP 在用户自定义函数中支持可变数量的参数列表.其实很简单,只需使用 func_num_args() , func_get_arg() ,和 fun ...
- java版gRPC实战之六:客户端动态获取服务端地址
欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...
- 旧书重温:0day2【4】动态获取函数地址
通过以上3篇文章的学习,我们已经可以获取到kernel32.dll的地址了下一步 我们就是获取几个重要的函数 1.GetProcAddress 2.LoadLibrary 有了这两个函数很多函数都可以 ...
- 获取函数的地址(三种方法,分别是@,Addr,MethodAddress)
问题来源: http://www.cnblogs.com/del/archive/2008/07/30/1039045.html#1272783 在编译器看来, 重载函数根本就是完全不同的几个函数, ...
- .Net CLR GC 动态加载短暂堆阈值的计算及阈值超量的计算
前言: 很多书籍或者很多文章,对于CLR或者GC这块只限于长篇大论的理论性概念,对于里面的如何运作模式,却几乎一无所知.高达近百万行的CPP文件,毕竟读懂的没有几个.以下取自CLR.Net 6 Pre ...
- php动态获取函数参数
PHP 在用户自定义函数中支持可变数量的参数列表.其实很简单,只需使用 func_num_args() , func_get_arg() ,和 func_get_args() 函数即可. 可变参数并 ...
- PHP实现动态获取函数参数的方法
1. func_num_args — 返回传入函数的参数总个数 int func_num_args ( void ) 示例 <?php function demo () { $numargs = ...
- 日志系统实战(二)-AOP动态获取运行时数据
介绍 这篇距上一篇已经拖3个月之久了,批评自己下. 通过上篇介绍了解如何利用mono反射代码,可以拿出编译好的静态数据.例如方法参数信息之类的. 但实际情况是往往需要的是运行时的数据,就是用户输入等外 ...
随机推荐
- c++对于c的扩展_冒号作用域
冒号作用域 ::(该运算符为作用域):如果::前面什么都没加代表全局作用域 #include <iostream> using namespace stu; int a=10; viod ...
- 【Oracle】EXPDP和IMPDP数据泵进行导出导入的方法
一.expdp/impdp和exp/imp 客户端工具 1.exp和imp是客户端工具程序,它们既可以在客户端使用,也可以在服务端使用. 服务端工具 2.expdp和impdp是服务端的工具程序,他们 ...
- 2021.08.06 P2441 角色属性树(树形结构)
2021.08.06 P2441 角色属性树(树形结构) P2441 角色属性树 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 题意: 求离x最近的祖先y且(x,y)>1. ...
- 超酷!!HTML5 Canvas 水流样式 Loading 动画
今天我们要分享另外一款基于HTML5 Canvas的液体流动样式Loading加载动画,这款Loading动画在加载时会呈现液体流动的动画效果,并且由于和背景颜色的对比,也略微呈现发光的动画效果. 效 ...
- pta L2-002 链表去重 +散列表知识小普及+二进制取反补码运算
题目链接:https://pintia.cn/problem-sets/994805046380707840/problems/994805072641245184: 废话:今天忙着学习新知识了,没怎 ...
- vue - vue基础/vue核心内容(终结篇)
今天是vue基础.vue核心内容第三天,也是最后一天,后面开始进入组件化学习,整个基础内容以生命周期的结束而结束,不得不说,张天禹把这节课讲活了,开始觉得vue是一个有生命的东西,包括前面所说的很多脏 ...
- kubeadm高可用master节点(三主两从)
1.安装要求 在开始之前,部署Kubernetes集群机器需要满足以下几个条件: 五台机器,操作系统 CentOS7.5+(mini) 硬件配置:2GBRAM,2vCPU+,硬盘30GB+ 集群中所有 ...
- spring 拦截器流程 HandlerInterceptor AsyncHandlerInterceptor HandlerInterceptorAdapter
HandlerInterceptor源码 3种方法: preHandle:拦截于请求刚进入时,进行判断,需要boolean返回值,如果返回true将继续执行,如果返回false,将不进行执行.一般用于 ...
- 第一个MVC程序
配置版 添加web的支持! 确定导入了SpringMVC 的依赖! 配置web.xml , 注册DispatcherServlet <?xml version="1.0" e ...
- 微信小程序云开发如何实现微信支付,业务逻辑又怎样才算可靠
今天打了几把永劫无间后,咱们来聊一聊用云开发来开发微信小程序时,如何实现微信支付,并且保证业务逻辑可靠. @ 目录 注册微信支付商户号 小程序关联商户号 业务逻辑 代码实现 注册微信支付商户号 点击& ...