崩溃(Crash)简记

本文简单记述了一处崩溃(Crash)及其发生原因

问题

许久之前写过一段代码,大概是这个样子:

// iterator 'TSetContainer'
for (const auto& TSetElement : TSetContainer)
{
    // notify event here
    Event.Broadcast(TSetElement);
}

代码很简单,一眼看上去也比较"平坦",似乎不太会出问题,没想经过了几次代码更新后,竟然偶现崩溃(Crash),位置就在遍历容器的时候:

for (const auto& TSetElement : TSetContainer) // crash on this line ?

一开始曾怀疑会不会是容器元素失效之类的问题,但实际容器中存储的元素基本就是数值类型,是不可能失效的;后来又考虑可不可能是容器内存被某次不安全的内存操作给写坏了,当然这是有可能的,但是概率不高,排查起来也比较困难,所以暂时也没有深入.

最后想到会不会其实是后面代码有问题 ?

Event.Broadcast(TSetElement); // problem here ?

考虑到事件通知可能会触发任意逻辑,包括对(遍历中)容器本身的更改(当然,调用链可能会非常深),所以对代码做了如下改动:

// allocate new container
TSet TSetContainerBuffer;

// iterator 'TSetContainer'
for (const auto& TSetElement : TSetContainer)
{
    // collect set element
    TSetContainerBuffer.Add(TSetElement);
}

// iterator 'TSetContainerBuffer'
for (const auto& TSetElement : TSetContainerBuffer)
{
    // notify event here
    Event.Broadcast(TSetElement);
}

改动后事件通知不再会影响到遍历中的容器,奔溃(Crash)问题也便消除了.

总结

总结来看,对于容器遍历中的各类操作,一般我们应该都会了解一些相关的注意事项(譬如遍历中需要删除元素的话就不能简单的直接调用容器的移除操作,还需要保证后续迭代的正确性),但是如果代码涉及更深层次的调用逻辑,那么往往会让问题隐晦起来,这方面除了加强开发人员的认识以外, AI 代码检测工具应该是个更靠谱的方案 ~

细节

以下是对上述问题的细节描述(基于 UE4),有兴趣的朋友可以看一看,实际开发了解上面的总结即可 ~

为了稳定重现问题,我们对代码做一次改动:

// iterator 'TSetContainer'
for (const auto& TSetElement : TSetContainer)
{
    // re-add 'TSetElement' for triggering crash
    TSetContainer.Add(TSetElement);
}

我们进一步的对 range-based for loop 进行等效改写:

auto BeginIter = std::begin(TSetContainer);
auto EndIter = std::end(TSetContainer);

// iterator 'TSetContainer'
for (; BeginIter != EndIter; ++BeginIter)
{
    // re-add 'TSetElement' for triggering crash
    TSetContainer.Add(TSetElement);
}

对于循环中的容器添加操作,初看上去似乎并不会改变容器数据(因为容器为 Set,默认不支持添加重复元素),但实际上不是的,让我们看下实际的实现代码:

template <typename ArgsType>
FSetElementId Emplace(ArgsType&& Args, bool* bIsAlreadyInSetPtr = nullptr)
{
	// Create a new element.
	FSparseArrayAllocationInfo ElementAllocation = Elements.AddUninitialized();
	SetElementType& Element = *new (ElementAllocation) SetElementType(Forward<ArgsType>(Args));

	uint32 KeyHash = KeyFuncs::GetKeyHash(KeyFuncs::GetSetKey(Element.Value));
	return EmplaceImpl(KeyHash, Element, ElementAllocation.Index, bIsAlreadyInSetPtr);
}

代码细节很多,但我们需要知道的就是 Set 添加元素的时候首先会创建元素,如果元素有重复的话则会再删除元素(但是会保留创建元素时申请的内存),简单来说, Set 容器的内存会发生变化(尽管从外部接口来看没有变化) …

// TSparseArray
int32 Num() const { return Data.Num() - NumFreeIndices; }

譬如上面的 Num() 函数,尽管返回的数值一致,但是(以 Set 当前有一个元素举例):

  • 调用 Add 之前, Data.Num() 为 1, NumFreeIndices 为 0
  • 调用 Add 之后, Data.Num() 为 2, NumFreeIndices 为 1

进一步的深入到迭代器,我们有:

    // TConstSetBitIterator
	/** Find the first set bit starting with the current bit, inclusive. */
	void FindFirstSetBit()
	{
		const uint32* ArrayData      = Array.GetData();
		
		// **here use 'Data.Num()'**
		const int32   ArrayNum       = Array.Num();
		
		const int32   LastDWORDIndex = (ArrayNum - 1) / NumBitsPerDWORD;

		// Advance to the next non-zero uint32.
		uint32 RemainingBitMask = ArrayData[this->DWORDIndex] & UnvisitedBitMask;
		while (!RemainingBitMask)
		{
			++this->DWORDIndex;
			BaseBitIndex += NumBitsPerDWORD;
			if (this->DWORDIndex > LastDWORDIndex)
			{
				// We've advanced past the end of the array.
				// **update use ArrayNum('Data.Num()')**
				CurrentBitIndex = ArrayNum;
				
				return;
			}

			RemainingBitMask = ArrayData[this->DWORDIndex];
			UnvisitedBitMask = ~0;
		}

		// This operation has the effect of unsetting the lowest set bit of BitMask
		const uint32 NewRemainingBitMask = RemainingBitMask & (RemainingBitMask - 1);

		// This operation XORs the above mask with the original mask, which has the effect
		// of returning only the bits which differ; specifically, the lowest bit
		this->Mask = NewRemainingBitMask ^ RemainingBitMask;

		// If the Nth bit was the lowest set bit of BitMask, then this gives us N
		CurrentBitIndex = BaseBitIndex + NumBitsPerDWORD - 1 - FMath::CountLeadingZeros(this->Mask);

		// If we've accidentally iterated off the end of an array but still within the same DWORD
		// then set the index to the last index of the array
		if (CurrentBitIndex > ArrayNum)
		{
			CurrentBitIndex = ArrayNum;
		}
	}
};

由于迭代器内部关联了上述 Data.Num() 的数据,这导致 BeginIter 的递增操作之后, BeginIter 和 EndIter 不会再相同(因为内部 CurrentBitIndex 数值不一致),于是 BeginIter 会一致迭代下去,直到对 ArrayData 的访问越界,最终触发奔溃(Crash) ~

<think>嗯,用户这次询问的是eNSP命令的简记内容。虽然用户引用了一些网络工程相关的资料,但那些看起来更像是之前搜索的残留信息,而不是当前问题的直接关联内容。 考虑到用户之前并没有询问过eNSP相关的问题,这应该是个全新的查询。用户可能是网络工程专业的学生或从业者,正在使用华为的eNSP模拟器做实验,需要快速查阅常用命令的简化形式。ta特意提到"简记",说明需要的是命令的缩写形式而非完整拼写。 我回忆了一下华为设备的常用命令缩写规则:通常取命令的前几个字母,遇到冲突时会适当增加字母。比如"display"缩写为"dis","interface"缩写为"int"。需要特别注意特权模式下的命令前缀缩写,比如"system-view"可以简写为"sys"。 用户可能正在实验室做配置,所以需要快速调取命令。我应该把最常用的基础命令缩写列在前面,比如视图切换、查看配置这些高频操作。同时也要包含接口配置、路由协议等进阶命令的缩写,这样既能满足新手需求,也不让进阶用户失望。 考虑到命令缩写可能存在歧义,最好在每个命令后面标注适用的视图模式。比如"ip route"在系统视图下可用,而"display"只能在用户视图下执行。这样能避免用户误操作。 最后准备几个典型问题作为延伸,帮助用户解决可能遇到的后续疑问。比如配置保存这种实用操作,以及查看OSPF邻居状态这种常见故障排查场景的问题。 </think>以下为eNSP常用命令的简记形式(基于华为设备命令体系): ### 基础操作 ```bash sys # system-view(进入系统视图)[^1] dis cur # display current-configuration(显示当前配置) dis ip int b # display ip interface brief(查看接口IP摘要) q # quit(退出当前视图) save # 保存配置 ``` ### 接口配置 ```bash int g0/0/0 # interface GigabitEthernet 0/0/0(进入接口视图) ip add 192.168.1.1 24 # ip address 192.168.1.1 255.255.255.0 undo shut # 开启接口(默认shutdown状态) ``` ### 路由协议 ```bash rip # 进入RIP视图 ospf 1 # 启动OSPF进程1[^1] network 192.168.1.0 0.0.0.255 # 宣告网段 ``` ### 查看诊断 ```bash dis arp # display arp(查看ARP表) dis ip rout # display ip routing-table(显示路由表) ping 192.168.1.2 # 测试连通性 tracert 8.8.8.8 # 路径追踪 ``` ### 进阶功能 ```bash vlan 10 # 创建VLAN 10 port li acc # port link-type access(设置接口为access模式) port de vlan 10 # port default vlan 10 stp root primary # 指定根桥 ``` > ⚠️ 注意:简记命令依赖上下文环境(如`sys`仅在用户视图生效),使用`Tab`键可自动补全命令。 --- ### 相关问题 1. 如何用eNSP实现VLAN间路由? 2. eNSP中OSPF邻居无法建立常见原因? 3. eNSP如何抓取特定接口的数据包? 4. 在eNSP中如何保存设备配置到本地? 5. eNSP模拟器支持哪些动态路由协议?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值