1.前言
card_table是CLR的核心技术之一,它的位标记循环遍历老年代堆(oldest_gen),找出老年堆对象对于新生代的引用,从CLR和GC里面萃取是一个较为复杂的工程,以.Net8为蓝本,本篇化繁为简,继续研看。
2.概述 老年代一般的都是指2代,也即是max_generation。循环遍历这个找出这个代里面的堆段(heap segment),通过位标记循环遍历每个堆段的引用对象的地址(老年代地址)范围,对被引用的对象进行存活标记。先上一张图,用以了解大致。
card_word和card_word_end范围遍历每一个2代堆段,找出2代堆段里引用的新生代对象。 3.论证 通过lldb查看下这一过程,首先是找到老年代对象(n1)和新生代对象(n2)的地址,两者处于前者引用后者。
Name n1 = new Name("1111");
GC.Collect();GC.Collect();
Name n2 = new Name("3333");
n1.selfName= n2;GC.Collect(0);
在托管Main的JIT_WriteBarrier代码处:
0x7fff78d85511: lea rdi, [rdi + 0x10]
0x7fff78d85515: mov rsi, qword ptr [rbp - 0x18]
0x7fff78d85519: call 0x7ffff730f6e0 ; JIT_WriteBarrier
两个对象地址如下:
(lldb) c
(lldb) register read rdi rsi
rdi = 0x00007fbf6a808b08
rsi = 0x00007fbf6cc00028
看下JIT_WriteBarrier干了啥
libcoreclr.so`JIT_WriteBarrier:
-> 0x7ffff730f6e0 <+0>: mov qword ptr [rdi], rsi
0x7ffff730f6e3 <+3>: mov r8, rdi
0x7ffff730f6e6 <+6>: movabs rax, 0x7fbee54ff2a0
0x7ffff730f6f0 <+16>: shr rdi, 0x16
0x7ffff730f6f4 <+20>: cmp byte ptr [rdi + rax], 0x0
0x7ffff730f6f8 <+24>: jne 0x7ffff730f6fe ; <+30>
0x7ffff730f6fa <+26>: rep ret
0x7ffff730f6fc <+28>: nop
0x7ffff730f6fe <+30>: movabs r9, 0x7fbf68000000
0x7ffff730f708 <+40>: cmp rsi, r9
0x7ffff730f70b <+43>: jae 0x7ffff730f70e ; <+46>
0x7ffff730f70d <+45>: ret
0x7ffff730f70e <+46>: movabs r9, 0x7fff68000000
0x7ffff730f718 <+56>: cmp rsi, r9
0x7ffff730f71b <+59>: jb 0x7ffff730f71f ; <+63>
0x7ffff730f71d <+61>: rep ret
0x7ffff730f71f <+63>: shr rsi, 0x16
0x7ffff730f723 <+67>: mov dl, byte ptr [rsi + rax]
0x7ffff730f726 <+70>: cmp dl, byte ptr [rdi + rax]
0x7ffff730f729 <+73>: jb 0x7ffff730f72e ; <+78>
0x7ffff730f72b <+75>: rep ret
0x7ffff730f72d <+77>: nop
0x7ffff730f72e <+78>: movabs rax, 0x7faedb5ff040
0x7ffff730f738 <+88>: mov ecx, r8d
0x7ffff730f73b <+91>: shr r8, 0xb
0x7ffff730f73f <+95>: shr ecx, 0x8
0x7ffff730f742 <+98>: and ecx, 0x7
0x7ffff730f745 <+101>: mov dl, 0x1
0x7ffff730f747 <+103>: shl dl, cl
0x7ffff730f749 <+105>: test byte ptr [r8 + rax], dl
0x7ffff730f74d <+109>: je 0x7ffff730f751 ; <+113>
0x7ffff730f74f <+111>: rep ret
0x7ffff730f751 <+113>: lock
0x7ffff730f752 <+114>: or byte ptr [r8 + rax], dl
0x7ffff730f756 <+118>: movabs rax, 0x7fbedf4ef500
0x7ffff730f760 <+128>: shr r8, 0xa
0x7ffff730f764 <+132>: cmp byte ptr [r8 + rax], -0x1
0x7ffff730f769 <+137>: jne 0x7ffff730f76d ; <+141>
0x7ffff730f76b <+139>: rep ret
0x7ffff730f76d <+141>: mov byte ptr [r8 + rax], -0x1
0x7ffff730f772 <+146>: ret
代码有点长,这里用C模拟下:
n1.selfName=n2;
r8=n1.selfName;
rax=cardw_card_bundle//在card_table基础上进一步限制范围
n1.selfName=n1.selfName>>0x16
if((cardw_card_bundle+n1.selfName)==0)
{
return;
}
else
{
if(短暂堆起始地址<=n2<短暂堆结束地址)
{
n2=n2<<0x16;
rax=0x7faedb5ff040;
*(rax+n1.selfName>>0xB)=8;//这个地方windows是0xFF
}
}
大致的意思就是把2代对象n1.selfName右移
0x0B+card_table首地址,它的值赋值为0x8.
继续:
br del
b gc.cpp:38448
c
38445 limit = min (end, card_address (end_card));
38446 #endif // FEATURE_CARD_MARKING_STEALING
38447 }
-> 38448 if (!foundp || (last_object >= end) || (card_address (card) >= end))
38449 {
38450 if (foundp && (cg_pointers_found == 0))
38451 {
limit上一篇说过,它是2代堆段里面的一个结束范围,还有一个起始范围,这两个变量构成了从这个范围内查找引用了新生代的老年代对象。看下这个limit的结束地址:
(lldb) p/x limit
(uint8_t *) $81 = 0x00007fbf6a808b18 ""
上面的n1.selfName的地址是:0x00007fbf6a808b08
而limit的结束范围地址是: 0x00007fbf6a808b18 可以此循环包含了2代对象的地址。那么它就可以找出来 4.find_card 在limit之前,CLR调用了find_card来查找这个2代对象引用新生代对象的地址范围。 在find_card函数里面去看下
b gc.cpp:37953
c
37952 last_card_word = &card_table [card_word (card)];
-> 37953 bit_position = card_bit (card);
37954 #ifdef CARD_BUNDLE
37955 // if we have card bundles, consult them before fetching a new card word
37956 if (bit_position == 0)
card_table就是上面第三步论证里面的n1.selfName右移之后加上的地址
(lldb) p/x card_table
(uint32_t *) $88 = 0x00007faedb5ff040
通过gc_heap::find_card_dword把2代堆段的结尾赋给card_word索引
37970 size_t lcw = card_word(card) + (bit_position != 0);
-> 37971 if (gc_heap::find_card_dword (lcw, card_word_end) == FALSE)
37972 {
37973 return FALSE;
37974 }
通过BitScanForward函数,获取位标记从右至左有几个0
DWORD bit_index;
38006 uint8_t res = BitScanForward (&bit_index, card_word_value);
-> 38007 assert (res != 0);
38008 card_word_value >>= bit_index;
38009 bit_position += bit_index;
这样就可以确定上面第三步论证里面的老年代n1.selfName位移之后加上card_table所在的地址里面的值是多少,然后计算出它的card
card = (last_card_word - &card_table[0]) * card_word_width + bit_position;
这样card到card_end,整个一个范围就确认了。然后循环遍历这个范围找出二代对象引用新生代对象的范围。对它进行标记。
这里还有一个要注意的地方就是除了card_table的循环,还有一个堆段的循环,前者在里小循环,后者在外大循环
if (seg)
{
#ifdef BACKGROUND_GC
should_check_bgc_mark (seg, &consider_bgc_mark_p, &check_current_sweep_p, &check_saved_sweep_p);
#endif //BACKGROUND_GC
beg = heap_segment_mem (seg);
#ifdef USE_REGIONS
end = heap_segment_allocated (seg);
#else
end = compute_next_end (seg, low);
#endif //USE_REGIONS
#ifdef FEATURE_CARD_MARKING_STEALING
card_word_end = 0;
#else // FEATURE_CARD_MARKING_STEALING
card_word_end = card_of (align_on_card_word (end)) / card_word_width;
5.标记
找到之后如何标记的呢?通过mark_object_simple
b mark_object_simple
c
n.....
24875
-> 24876 o = mark_queue.queue_mark (o);
24877 if (o != nullptr)
24878 {
24879 m_boundary (o);
qeeue_mark查找标记队列,如果这个对象已经被标记了,那么就不需要再标记。
看下n2是否被标记
(lldb) x/8gx 0x00007fbf6cc00028
0x7fbf6cc00028: 0x00007fff7911c471 0x00007fffe6bff8e0
0x7fbf6cc00038: 0x0000000000000000 0x0000000000000000
0x7fbf6cc00048: 0x0000000000000000 0x0000000000000000
0x7fbf6cc00058: 0x0000000000000000 0x0000000000000000
因为是跟对象已经被标记,所以这里不需要再次标记,以上大致跨代引用的操作,细节之处依然值得继续探究