共计 13480 个字符,预计需要花费 34 分钟才能阅读完成。
背景
家喻户晓,堆 crash dump 是最难剖析的 dump 类型之一。此类 crash 最大的问题在于,造成谬误的代码无奈在产生堆毁坏时被发现。线上采集到的 minidump,仅能提供非常无限的信息。当调试工具报告了堆毁坏、堆内存拜访违例后,即使是有教训的开发人员也会感觉头疼。剪映专业版及其依赖的音视频编辑 SDK、特效模块均采纳 MD 的形式链接规范库,这意味着任何一个模块呈现了堆损坏都会相互影响。从 crash 的地位回溯堆毁坏的源头,是一个十分有挑战性的工作。剪映业务模块较常见的是Use-after-free
,而音视频编辑 SDK 和特效模块这类底层算法特效模块更多的是Buffer-overflow
,不同团队模块间的堆谬误相互影响,导致问题难以定位。
GWP-ASan 是 Google 主导开发的用于检测堆内存问题的调试工具。它基于经典的 Electric Fence Malloc 调试器 原理,概率采样内存调配行为,抓取内存问题并生成上传解体报告。说到这里,兴许你会好奇它和 ASan(Address Sanitizer)的区别。ASan 是一种编译器调试工具,监控所有内存调配行为,能够发现栈、堆和全局内存问题,但它性能开销很高(2- 3 倍),不适宜线上应用。GWP-ASan 相较于 ASan,尽管无奈发现栈内存和全局内存问题,但因为它是采样监控,性能耗费能够忽略不计,更实用于线上场景。目前,GWP-ASan 可检测的谬误有:
Use-after-free
Buffer-underflow
Buffer-overflow
Double-free
-
free-invalid-address
Electric Fence Malloc 调试器:https://linux.die.net/man/3/efence
GWP-ASan 有多种实现计划,本计划基于 Windows 平台阐明,字节外部 APM-PC 平台相较于市面上其余计划的亮点有:
- 无侵入式接入,能够检测特定类型三方库的内存调配。
- 反对无感知监测,发现异常后过程可持续运行。
- 反对调整检测所用的堆页面个数配置和采样率配置,灵便调整性能耗费。
剪映专业版接入字节外部 APM-PC 平台的 GWP-ASan 性能后,帮忙业务、音视频编辑 SDK、特效模块解决 30 余例疑难堆 crash。GWP-ASan dump 比原生 dump 提供了更丰盛的信息,并指出了堆 crash 关联的信息细节,升高了疑难 crash 的排查难度,无效缩短了研发排查、修复问题的工夫。
技术计划
监控原理
检测原理概述
- 创立受爱护内存池:
首先,咱们须要保留一块间断的 n*page size
的受爱护内存池。其中,可分配内存的 page 是 Slot
,不可分配内存的 page 是Guard Page
。Slot
和Guard Page
距离散布,整个内存池最前和最初都是 Guard Page
,所有的Slot
都受到 Guard Page
爱护,之后利用调配的堆内存将随机采样调配到这些 Slot
上。
- 采样监控内存调配行为,记录堆栈:
之后,hook 利用堆内存调配行为,每次调配堆内存时,随机决定指标内存是走 GWP-ASan 调配——调配在一个闲暇的 Slot
上,还是走零碎原生调配。如果走 GWP-ASan 调配,那么指标内存会被随机左对齐 / 右对齐调配在一个闲暇的 Slot
上,同时记录分配内存的堆栈信息。
而当开释内存时,会先判断指标内存是否在 GWP-ASan 受爱护内存池上,如果是,那么开释这块内存和其所在的 Slot,同时记录开释内存的堆栈。slot 闲暇后,能够从新被用于调配。堆栈信息记录在 metadata 中。
-
继续监测,记录异样:
- 首先,咱们须要晓得
Guard Page
和闲暇的Slot
都是不可读写的。接下来咱们看看 GWP-ASan 是如何发现异常的: Use-after-free
:Slot
上未分配内存时,是不可读写的。当拜访到不可读写的Slot
时,利用抛出异样,此时查看该Slot
是否刚开释过内存,如果开释过内存,那么能够断定此异样为Use-after-free
。Buffer-underflow
:当内存左对齐调配在Slot
上时,如果产生了 underflow,利用会拜访到Slot
左侧不可读写的Guard Page
,利用抛出异样,此异样为Buffer-underflow
。Buffer-overflow
:当内存右对齐调配在Slot
上时,如果产生了 overflow,利用会拜访到Slot
右侧不可读写的Guard Page
,利用抛出异样,此异样为Buffer-overflow
。Double-free
:利用开释内存时,首先查看指标内存地址是否位于受爱护内存池区间内,如是,由 GWP-ASan 开释内存,开释前查看指标内存地址所在Slot
是否曾经被开释,如是,那么能够断定此异样为Double-free
。Free-invalid-address
: 利用开释内存时,首先查看指标内存地址是否位于受爱护内存池区间内,如是,由 GWP-ASan 开释内存,开释前先查看要开释的内存地址和之前调配返回的内存地址是否相等,如果不相等,那阐明指标开释地址是非法地址。此异样为Free-invalid-address
。
- 首先,咱们须要晓得
堆内存调配 API
后面曾经提到,GWP-ASan 用于检测堆内存问题,为了检测堆内存问题,必须先感知利用内存调配行为。很天然的,咱们会想到 hook 内存调配办法,然而该 hook 哪个办法呢?
下图形容了 Windows 利用调配堆内存的可用办法:
GlobalAlloc/LocalAlloc
是为了兼容 Windows 旧版本的 API,当初根本不实用,所以不监控。HeapAlloc/HeapFree
个别用于过程分配内存,不监控。VirtualAlloc
是应用层内存调配的底层实现,开发个别不间接用此 API 分配内存,它离利用调配堆内存行为太远,堆栈参考意义不大;且 Windows GWP-ASan 须要基于此实现,因而,也不监控。
最终选定 Hook malloc/free
等系列办法,hook malloc/free
后,能感知到用户调配的堆内存。
### Hook 计划
上面的计划都是应用层的 Hook 计划,内核层 Hook 仅实用于 x86 平台。
Detours 库作为微软官网出品的 hook 库,兼容性佳,稳定性好,是最佳抉择。然而还须要留神的是,Windows 下,运行时库配置会影响 hook 后果,Detours 只能无侵入式 hook/MD 库的内存调配行为,/MT 库须要提供本身内存调配的函数指针能力 hook。
堆栈记录
首先要阐明的是,GWP-ASan 监控依赖解体监控。Use-after-free
、Buffer-underflow
、Buffer-overflow
都是在客户端产生异样后,联合 GWP-ASan 的 metadata 去断定的。目前字节外部 APM-PC 平台的解体报告格局为 minidump。一个 minidump 文件由多种 streams 组成,如 thread_list_stream、module_list_stream 和 exception_stream 等等。不同 stream 记录了不同信息,咱们能够将 GWP-ASan 采集到的异样信息视为独自的 gwpasan_stream,附加到 minidump 文件中。
GWP-ASan 采集的信息次要包含:谬误类型、调配地址和大小、调配堆栈、开释堆栈(如有)、受爱护内存池起止地址。这些信息基于 Protobuf 协定序列化后,被增加到 minidump 文件中。GWP-ASan 通过 Windows native API CaptureStackBackTrace
API 在客户端回溯“开释 / 调配”堆栈。minidump 上传到平台后,平台抽取出 GWP-ASan 信息,联合 minidump 中 loaded module list,联合相干模块的符号表,符号化 GWP-ASan 调配 / 开释堆栈。GWP-ASan 信息联合 minidump 本来的信息,根本就能定位问题。
监控流程
拓展场景
无解体计划
GWP-ASan 检测到异样后,会被动解体导致客户端过程退出,给用户带来了不良体验。无解体的 GWP-ASan 检测到异样后,再将对应内存页标注为可读写的(如为 use-after-free/buffer-underflow/buffer-overflow),仅生成上传解体报告,不被动终结过程,客户端标注异样已解决。用户无感知,程序持续运行。须要留神的是,客户端在 UEF 里标记拜访区域内存页为可读写内存页可能影响后续的 GWP-ASan 检测。
实战分享
Use-After-Free:开释后应用
理论案例 1
咱们看下惯例的 dump 输入,windbg 告知咱们程序 crash 在 25 行。
因为 12 行有空指针查看,能够排除空指针问题。
执行 .ecxr
复原异样现场也能够证实,此 crash 和空指针无关。只是一个内存拜访违例。
汇编指定地址,能够晓得这个 crash 动作是在读取类的虚指针,读取内存的过程中 crash 了。
00007ffb`d422e4a0 498b06 mov rax,qword ptr [r14]
00007ffb`d422e4a3 488bd5 mov rdx,rbp
00007ffb`d422e4a6 498bce mov rcx,r14
00007ffb`d422e4a9 ff10 call qword ptr [rax]
查看问题代码:
class VENotifyListenerBase {
public:
virtual void notify(const VENotifyData& data) = 0;
};
// 辅助注册类
class VENotifyListener : public VENotifyListenerBase
{
public:
VENotifyListener (){VENotify:: instance (). addListener (this);
}
virtual ~ VENotifyListener () {VENotify:: instance (). removeListener (this);
}
};
void VENotify::notify(const VENotifyData& data)
{
++m_nested;
std::atomic<char*> info = nullptr;
for (size_t index = 0; index < m_listeners.size(); ++index) {auto listener = m_listeners[index];
if (!listener) {
++m_invaildCount;
continue;
}
...
listener-> notify (data); // crash 点
}
--m_nested;
...
}
很多类继承了 VENotifyListener
这个帮忙类。剖析这个帮忙类,咱们比拟容易得出结论VENotify
的变量 m_listeners
线程不平安,当 VENotify::removeListener
和VENotify::notify
存在竞争时,就可能会呈现这个 crash。这个论断是靠咱们的教训得出的,咱们能够加个锁,搞定这个竞争导致的 crash。
那么这个问题的确解决了么?如果咱们没有 GWP-ASan,咱们很可能会止步于此,匆匆修复 crash 并提交代码,拍着胸脯说,我搞定了。
仔细的同学可能会发现,有人可能会不继承 VENotifyListener
,而是继承VENotifyListenerBase
, 间接调用VENotify::instance().addListener
和VENotify::instance().removeListener
,检索工程代码可能会发现一堆 addListener
和removeListener
,更可怜的是,可能会发现 addListener
和removeListener
都是成对呈现的。到底是谁应用不标准导致的 crash 呢?接下来咱们只能一一查看代码,或者深刻调试找到问题地位。这么做可能须要破费较多的工夫。
侥幸的是,GWP-ASan 也抓到同地位的 crash 了,咱们看下 GWP-ASan 的 crash 输入:
USE AFTER FREE
*******.dll VENotify::notify
*******.dll QMetaObject::metacall
*******.dll QQmlObjectOrGadget::metacall
GWP-Asan Info
Error type:USE AFTER FREE
Allocation address:0x1866ff827b20
Allocation size:1240
GWPASan region start:0x1866ddb10000
GWPASan region size:0x12c001000
Valid memory range:[0x1866ff827b20, 0x1866ff827ff8)
GWP-ASan 确切的告知咱们此处 crash 起因是 UAF,并通知了咱们很多的细节信息。那么是谁在什么时候被开释的?
GWP-ASan 的 Free Stack 页面告知咱们是 MediaInfoViewModel
导致的问题,咱们查看 MediaInfoViewModel
代码发现有如下代码:
void MediaInfoViewModel::EnableNotify(bool enable) {if (enable) {VENotify::instance().addListener(this);
} else {VENotify::instance().removeListener(this);
}
}
果然,业务本人调用了 VENotify::instance().addListener
,然而 MediaInfoViewModel
析构前并没有保障肯定会调用 VENotify::instance().removeListener
。这种状况下,意味着 VENotify::instance()
持有了一个 MediaInfoViewModel*
的悬垂指针,等到下次 notify
调用,就会 crash。
修复计划:
- 确保
MediaInfoViewModel
在析构前会调用VENotify::instance().removeListener
; - 对存在线程间竞争的中央加锁爱护。
理论案例 2
首先咱们看下惯例的 dump 输入,windbg 告知咱们 crash 在 QT 和 std 规范库中,std 规范库鲜有 bug,此处必定不是第一现场,QT 尽管潜在的有 bug,但实际上 bug 也是比拟少的。这应该又是一个堆 crash。
切换栈帧到 08 查看代码,发现 QUICollectionViewItem
是一个多叉树的数据结构。
调试器告知咱们,此 crash 的确是一个堆 crash,在枚举成员变量的时候挂掉了。此时的 this 指针指向的地位曾经呈现了问题,曾经不再是失常的地址了。查看 this 指针指向的地址能够证实这一点。
因为不是第一现场,咱们须要思考什么状况,会导致此问题。堆溢出,内存踩踏,UAF 都能够导致此问题。
不过依据教训来看,针对这种指针比拟多的数据结构,UAF 的概率比拟高,然而没人敢拍着胸脯说这个 crash 肯定是 UAF 导致的。
GWP-ASan 再次抓到了此问题,GWP-ASan 的报告如下:
USE AFTER FREE
********.dll QUICollectionViewItem::clearSubitems
********.dll DraftTemplatePageControl::updateSearchCategoryViewModel
********.dll QMetaObject::invokeMethodImpl
GWP-Asan Info
Error type:USE AFTER FREE
Allocation address:0x2198e2a4bf80
Allocation size:128
GWPASan region start:0x2198300b0000
GWPASan region size:0x12c001000
Valid memory range:[0x2198e2a4bf80, 0x2198e2a4c000)
GWP-ASan 再次明确的的告知咱们此处 crash 起因是 UAF,此时咱们只有集中精力查看 UAF 方可。那么是谁开释了QUICollectionViewItem
?
上图 Free Stack 页面显示 QUICollectionViewItem
是在 QT 音讯循环中被析构的,尽管是 QUICollectionViewItem
析构的第一现场,但不是代码级别的第一现场。理解 QT 的同学晓得,调用了 deleteLater()
才会有此堆栈。为了解决 crash,咱们还须要找到调用 deleteLater()
的中央,最初找到如下代码段:
void QUICollectionViewItem::slotTreeItemWillDistory()
{if (m_parentItem != nullptr) {m_parentItem->removeSubitem(this);
...
}
...
deleteLater();}
回顾一下咱们的 crash 以及 UAF,实际上父节点持有了悬垂指针并调用 clearSubitems()
,程序就会挂掉。此处的代码看似从m_parentItem
中移除了本节点 (注:m_parentItem->removeSubitem(this)
),然而如果代码不谨严(如m_parentItem
在某种状况下被设置为 nullptr
),那么就可能存在悬垂指针。咱们查看谁会批改m_parentItem
,且要重点查看谁会将m_parentItem
批改为nullptr
。
查看代码会发现只有一个函数会批改m_parentItem
,代码如下:
void QUICollectionViewItem::setParentItem(QUICollectionViewItem* parentItem)
{IF_RETURN_VOID(m_parentItem == parentItem);
m_parentItem = parentItem;
IF_RETURN_VOID(m_parentItem != nullptr);
if (m_inVisualArea || m_collectionView->alwaysKeepItems()){...}
...
}
留神上述代码没有解决 m_parentItem
变更的状况,此时咱们找到问题地位。
修复计划:
当一个节点的父节点要变更时,须要从旧的父节点中摘除本人,防止旧的父节点持有子节点的悬垂指针。
void QUICollectionViewItem::setParentItem(QUICollectionViewItem* parentItem)
{IF_RETURN_VOID(m_parentItem == parentItem);
if (m_parentItem) {m_parentItem-> removeSubitem ( this);
}
m_parentItem = parentItem;
...
}
理论案例 3
首先咱们看下惯例的 dump 输入,windbg 再次提醒咱们 crash 在规范库相干操作了。
void XXXXXX_class::checkRequestCompleted()
{if (resource_request_status_map_.empty())
return;
for (auto iter = resource_request_status_map_.begin(); iter != resource_request_status_map_.end(); ++iter) {if (!iter->second.first || !iter->second.second)
return;
}
...
}
到底是什么问题导致的 crash?这代码看着也很简略,一般的 dump 没有再提供更多的信息~
iter
空指针?XXXXXX_class
被析构?多线程竞争?UAF?溢出?咱们不得不猜想,并查看代码,或者进一步剖析 dump 来验证咱们的想法。
咱们再看下 GWP-ASan 提供的信息,GWP-ASan 报告如下:
USE AFTER FREE
******.dll XXXXXX_class::responseToGetEffectListByResourceIds
******.dll davinci::effectplatform::loki::FetchEffectsByIdsTask::onFailed
******.dll VECORE::NetClient::request
GWP-Asan Info
Error type:USE AFTER FREE
Allocation address:0x1f662391bfc0
Allocation size:56
GWPASan region start:0x1f6510c40000
GWPASan region size:0x12c001000
Valid memory range:[0x1f662391bfc0, 0x1f662391bff8)
能够看到对于同一个规范库的数据结构,同时有三个线程在拜访。此时咱们明确的晓得,此 crash 是因为多线程竞争导致的。而且 GWP-ASan 明确输入了数据结构的开释堆栈,咱们不必再去猜想及思考问题是如何导致的。
修复计划:
非常简单,对存在竞争的数据结构加锁方可。
Buffer-overflow:内存溢出
理论案例 1
咱们还是看下惯例 dump 提供的信息:
dump 批示解体在了 share_ptr 减少援用计数的中央。大家都晓得 share_ptr 的援用计数是保留在堆外面的,咱们又遇到堆问题了。
static std::vector< int64_t > getKeyframeTrimDeltaList (std::shared_ptr<SegmentT> video_segment) {
std::vector<int64_t> trimDeltaList;
...
return trimDeltaList;
}
//crash 的函数
std::vector< int64_t > ExecutorHelper::getKeyframeSeqDeltaList (const std::shared_ptr<SegmentVideo>& segment) {
...
auto trimDeltaList = getKeyframeTrimDeltaList(segment);
...
}
template<typename SegmentT>
std::vector<int64_t> get_keyframe_seq_delta_list(const std::shared_ptr<Draft>& draft,
const std::shared_ptr<SegmentT> &segment) const {
...
auto ret = ExecutorHelper::getKeyframeSeqDeltaList(segment);
...
}
const std::vector<int64_t> VideoSettingsData::updateKeyframeSeqTimeList(size_t index, bool force)
{if (index >= m_segmentPtrs.size() || index >= m_keyframeSeqOffsetTimelists.size()) {assert(false);
}
auto & seg = m_segmentPtrs[index];
assert(seg);
...
}
void VideoSettingsData::setSegmentIds(const std::vector<std::string>& segIds)
{
...
if (auto query_utils = LvveQueryUtils) {for (size_t i = 0; i < m_segmentIds.size(); ++i) {
...
auto segmentPtr = ....;
...
IF_CONTINUE(segmentPtr == nullptr)
m_segmentPtrs. push_back (segmentPtr);
updateKeyframeSeqTimeList (i, true);
}
}
}
如果没有 GWP-ASan 的帮忙,大家看下问题在什么中央?没有排查教训的话,同学们可能就折在崩溃点的左近的代码了,而后百思不得其解。即使有排查教训的,同学们亦须要逐帧去查看代码实现,还得了解代码实现, 最初定位问题地位。
咱们看下 GWP-ASan 的输入:
BUFFER OVERFLOW
*******.dll VideoSettingsData::updateKeyframeSeqTimeList
*******.dll QMetaCallEvent::placeMetaCall
*******.dll QApplicationPrivate::notify_helper
GWP-Asan Info
Error type:BUFFER OVERFLOW
Allocation address:0x3a3d230a3fe0
Allocation size:32
GWPASan region start:0x3a3ca3fd0000
GWPASan region size:0x12c001000
Valid memory range:[0x3a3d230a3fe0, 0x3a3d230a4000)
可见 GWP-ASan 告知咱们是堆溢出,并且替咱们定位到了第一现场。咱们只有查看 ViedoSettingsData.cpp
803 行四周的代码,就能迅速定位问题。也就是上述代码的 auto& seg = m_segmentPtrs[index];
这段代码导致了溢出。再查看上一层函数,发现当 IF_CONTINUE(segmentPtr == nullptr)
时,必然会呈现堆越界。
void VideoSettingsData::setSegmentIds(const std::vector<std::string>& segIds)
{
...
m_segmentIds = segIds;
...
for (size_t i = 0; i < m_segmentIds.size(); ++i) {
...
IF_CONTINUE (segmentPtr == nullptr)
m_segmentPtrs. push_back (segmentPtr);
updateKeyframeSeqTimeList (i, true);
}
}
修复计划:
解除 updateKeyframeSeqTimeList
的越界操作。
理论案例 2
此处代码看起来比较复杂,为了不便了解,此处只保留剖析 crash 相干的代码。本 crash 咱们外部无奈复现。但外部 APM-PC 平台监控到的 crash 还不少。
bool EncryptUtilsImpl::getOriginEncryptText(const char *encryptText,
...) {
...
int length = strlen(encryptText);
...
*withOutKeyEncryptText = (char *) malloc (length - LENGTH1 - LENGTH2 + 1);
...
int32_t pre_length = 0;
int32_t pre_location = 0;
...
for (int it = 0; it < XXXXXXXX.size(); it++) {
...
if (XXXXXX) {
...
pre_length += XXXXXX;
pre_location = XXXXXX;
}
...
}
memcpy (*withOutKeyEncryptText + pre_length,
xxxxxx+ pre_length + xxxxxx,
xxxxxx- pre_location + xxxxxx);
return true;
}
关上惯例 dump 查看输入:
dump 显示 crash 在函数开端的 memcpy 中,真的很侥幸,尽管是堆相干的问题,然而咱们 crash 在了第一现场。
ExceptionAddress: 00007ffa16b715f0 (VCRUNTIME140!memcpy+0x0000000000000300)
ExceptionCode: c0000005 (Access violation)
粗略的看这个代码也没什么问题,排查问题的时候,咱们如果能失去局部变量pre_length
pre_location
length
的值,就能够晓得为什么 crash 了。
查看以后栈帧的局部变量,如下图:
十分可怜,咱们没法看到 length 的值,release 版本曾经将这个局部变量给优化掉了。
如果咱们不是作者的话,不理解程序逻辑,当察看到 char *
`encryptText` = 0x00000254a6a6e870 "U???"
,咱们很可能会狐疑是堆毁坏了(前面理解了代码逻辑后晓得,这个中央内存是正确的)。咱们针对问题用户独自开启了 GWP-ASan,很快 GWP-ASan 捕捉到同地位的 crash。
GWP-ASan 输入如下:
BUFFER OVERFLOW
*******.dll EncryptUtilsImpl::getOriginEncryptText
*******.dll EncryptUtilsImpl::decrypt
*******.exe VELauncher::exec
GWP-Asan Info
Error type:BUFFER OVERFLOW
Allocation address:0x293a8fd5000
Allocation size: 68 #要害信息,缺失的 length 信息
GWPASan region start:0x293a5500000
GWPASan region size:0x12c001000
Valid memory range:[0x293a8fd5000, 0x293a8fd5044)
下图是 GWP-ASan 捕捉的 dump,windbg 解析输入的内容:
留神:咱们一共申请了Allocation size:68 个字节的内存:
// length - LENGTH1 - LENGTH2 + 1 = 68
*withOutKeyEncryptText = (char *) malloc (length - LENGTH1 - LENGTH2 + 1);
然而当初int pre_length = 0n83
:
memcpy(*withOutKeyEncryptText + pre_length,
xxxxxx+ pre_length + xxxxxx,
xxxxxx- pre_location + xxxxxx);
显然,*withOutKeyEncryptText + pre_length
当初越界了。
void EncryptUtilsImpl::decrypt(const char *encryptText, char **outEncryptText, const std::string& from) {
...
getOriginEncryptText(encryptText, ...);...
}
std::string EncryptUtilsImpl::decrypt(const std::string& encryptStr, ...) {
...
const char *input_str = encryptStr. data ();
if (strlen(input_str) > 0) {EncryptUtilsImpl:: decrypt (input_str,...);
}
...
}
咱们回溯代码,最终发现,原来是实现形式上有点问题。咱们将 encryptStr
当作一个 buffer 应用,encryptStr
外部保留的不肯定是字符串。换句话说本函数的第一个参数 const char *encryptText
并不是个字符串,而是个二进制流。然而 EncryptUtilsImpl::getOriginEncryptText()
外部却对 encryptText
进行了 int length = strlen(encryptText)
操作。此时,如果 encryptText
二进制数据流中很可怜提前呈现了 0,那么这个中央就会呈现堆溢出 crash。
修复计划:
不再应用 const char *input_str = encryptStr.data();
的模式传裸指针给函数。而是抉择间接传const std::string& encryptStr
,此时 std::string 会携带了正确的数据长度,问题得以解决。
Reference
- Windows 常见内存治理办法
- Comparing Memory Allocation Method
- [https://chromium.googlesource.com/chromium/src.git/+/HEAD/doc…](https://chromium.googlesource.com/chromium/src.git/+/HEAD/doc…
) - https://sites.google.com/a/chromium.org/dev/Home/chromium-security/articles/gwp-asan
理解更多
无关 PC 端监控的能力,咱们已将局部性能在火山引擎利用性能监控全链路版中对外提供,你可增加下方小助手或点击“申请链接”申请收费试用。
- 增加小助手,回复【APM】退出技术交换群
- 申请链接:https://wenjuan.feishu.cn/m/cfm?t=s6RJBByEbWvi-fmpz*