llvm进阶(2)LowerSwitch(Switch 到 Branch 的奥秘)

admin 2025年4月6日19:09:55评论7 views字数 10930阅读36分26秒阅读模式

在编译器的优化过程中,Switch`指令的处理是一个既经典又充满挑战的问题。Switch`作为一种高效的多路分支结构,广泛存在于高级语言的代码中,但在底层实现中,不同硬件平台对 Switch`指令的支持程度却大相径庭。为了兼容性和性能优化,LLVM 引入了 LowerSwitch 这一重要转换:它将 Switch 指令重写为一系列 Branch指令,从而让目标平台可以暂时规避对 Switch指令的直接支持,直到更方便时再实现。

本文将简要解读 LowerSwitch 的部分源码,和看一下实现后的结果。

# 1.runOnfunction

```c

bool LowerSwitchLegacyPass::runOnFunction(Function &F) {

//获取 LazyValueInfo 对象,用于值推断和优化

LazyValueInfo *LVI = &getAnalysis<LazyValueInfoWrapperPass>().getLVI();

//尝试获取 AssumptionCacheTracker 对象,用于管理 AssumptionCache

auto *ACT = getAnalysisIfAvailable<AssumptionCacheTracker>();

//获取与函数 F 相关的 AssumptionCache 对象,如果 AssumptionCacheTracker 不存在,则设置为 nullptr

AssumptionCache *AC = ACT ? &ACT->getAssumptionCache(F) : nullptr;

return LowerSwitch(F, LVI, AC);

}

```

开篇中已经描述lowerSwitch的作用,在runOnfunction函数中,看到传入了LazyValueInfo、AssumptionCache,在这里由于篇幅原因,介绍下这两个类的用途,和在swithtobranch的作用。

## 1.1LazyValueInfo 的作用

`LazyValueInfo` 是 LLVM 中的一个分析工具,用于“惰性”地计算某些值的信息。在 `LowerSwitch` 中,`LazyValueInfo` 的作用主要体现在以下方面:1.值范围分析:LowerSwitch 需要分析 Switch 指令中每个 case 的值范围,以确定如何将 Switch 转换为 Branch。LazyValueInfo 提供了高效的值范围分析能力,帮助 LowerSwitch 快速判断哪些 case 可以被合并或优化。2.条件优:在某些情况下,Switch 的条件可能可以被简化。例如,如果某个 case 的值范围已经被 LazyValueInfo 确定为不可能,LowerSwitch 可以直接跳过该分支,从而减少生成的 Branch 指令数量。

## 1.2AssumptionCache的作用

`AssumptionCache` 是 LLVM 中用于管理“假设”的工具。在 `LowerSwitch` 中,`AssumptionCache` 的作用主要体现在以下方面:1.假设利用LowerSwitch 可以利用 AssumptionCache 中记录的假设,例如“某个变量的值范围已经被限定”,从而优化 Switch 的转换过程。例如,如果 AssumptionCache 中记录了某个变量的值不可能为负数,LowerSwitch 可以忽略所有负数的 case,从而简化生成的代码。2.代码安全性AssumptionCache 还可以帮助 LowerSwitch 确保转换后的代码在语义上是安全的。例如,如果某个假设被违反,LowerSwitch 可以生成额外的检查代码,避免运行时错误。

# 2.LowerSwitch

2.1遍历函数的基本块

LowerSwitch 会遍历函数的所有基本块(BasicBlock),检查每个基本块的终止指令是否是 SwitchInst。如果是,则对其进行处理。

2.2处理 Switch 指令

对于每个 SwitchInst,LowerSwitch 会调用 ProcessSwitchInst 函数,将其转换为更简单的分支结构。同时,可能会将一些基本块标记为待删除。

2.3清理待删除的基本块

在处理完所有 SwitchInst 后,LowerSwitch 会遍历待删除的基本块列表,调用 DeleteDeadBlock 函数将其删除,并更新 LazyValueInfo 中的相关数据。

```c

bool LowerSwitch(Function &F, LazyValueInfo *LVI, AssumptionCache *AC) {

// 用于记录函数 F 是否被修改过

bool Changed = false;

// 用于存储需要删除的基本块(BasicBlock)的集合

// SmallPtrSet 是一个优化过的集合类,适用于存储少量元素(这里容量为 8)

SmallPtrSet<BasicBlock *, 8> DeleteList;

// 遍历函数 F 的所有基本块

// Function::iterator I 是当前迭代器,E 是结束迭代器

for (Function::iterator I = F.begin(), E = F.end(); I != E;) {

// 获取当前基本块 Cur,并将迭代器 I 向前移动

// 这样可以在处理过程中跳过新添加的基本块

BasicBlock *Cur =

&*I++; // Advance over block so we don't traverse new blocks

// 如果当前基本块 Cur 在 DeleteList 中,表示它已经被标记为待删除

// 跳过处理,避免重复操作

if (DeleteList.count(Cur))

continue;

// 检查当前基本块 Cur 的终止指令是否是 SwitchInst

// 如果是,则进行处理

if (SwitchInst *SI = dyn_cast<SwitchInst>(Cur->getTerminator())) {

// 标记函数 F 被修改过

Changed = true;

// 处理 SwitchInst,可能会将 SwitchInst 转换为更简单的分支结构

// 同时,可能会将一些基本块标记为待删除,并加入 DeleteList

ProcessSwitchInst(SI, DeleteList, AC, LVI);

}

}

for (BasicBlock *BB : DeleteList) {

LVI->eraseBlock(BB);

DeleteDeadBlock(BB);

}

return Changed;

}

```

接下来着重分析ProcessSwitchInst

# 3.ProcessSwitchInst

将指定的 `switch` 指令替换为一系列通过平衡二叉搜索链接的 `if-then` 指令。

`ProcessSwitchInst` 函数有 **4 个参数**,具体如下

SwitchInst *SI:这是一个指向 SwitchInst 类型对象的指针,表示当前正在处理的 switch 指令。

SmallPtrSetImpl<BasicBlock *> &DeleteList:这是一个引用,指向一个存储 BasicBlock 指针的集合(SmallPtrSet),用于记录需要删除的基本块。

AssumptionCache *AC:这是一个指向 AssumptionCache 类型对象的指针,用于缓存和查询假设信息,帮助优化过程。

LazyValueInfo *LVI:这是一个指向 LazyValueInfo 类型对象的指针,用于延迟计算和查询值的信息,辅助优化决策。

## 3.1标记并删除不可达或自循环的基本块

内容和标题说的一样

```c

// 获取 switch 指令所在的基本块

BasicBlock *OrigBlock = SI->getParent();

//获取该基本块所属的函数

Function *F = OrigBlock->getParent();

//获取 switch 指令的条件值(即 switch 中用于比较的值)

Value *Val = SI->getCondition(); // The value we are switching on...

// 获取 switch 指令的默认目标基本块(即 default 分支)

BasicBlock* Default = SI->getDefaultDest();

// 不处理不可达的基本块。如果这些基本块的后继中有 PHI 节点,

// 直接删除会导致这些 PHI 节点缺少前驱。

if ((OrigBlock != &F->getEntryBlock() && pred_empty(OrigBlock)) ||

OrigBlock->getSinglePredecessor() == OrigBlock) {

DeleteList.insert(OrigBlock);

return;

}

```

OrigBlock != &F->getEntryBlock()

检查 OrigBlock 是否是函数的入口块(Entry Block)。入口块是函数的起始块,不能被删除。

pred_empty(OrigBlock)

检查 OrigBlock 是否有前驱块。如果没有前驱块,说明 OrigBlock 无法被访问到。

OrigBlock->getSinglePredecessor() == OrigBlock

检查 OrigBlock 的唯一前驱块是否是它自己。如果是,说明 OrigBlock 形成了一个自循环。

DeleteList.insert(OrigBlock)

如果 OrigBlock 满足上述任一条件,则将其插入到 DeleteList 中,后续会统一删除这些基本块。

## 3.2  Switch 分支聚类与默认分支排除(对switch进行一个case值的优化)

```c

// Prepare cases vector.

//// 准备 cases 向量,用于存储 switch 的所有分支信息

CaseVector Cases;

// 调用 Clusterify 函数,将 switch 的分支聚类并填充到 Cases 向量中,

// 同时返回简单分支的数量(即非默认分支的数量)

const unsigned NumSimpleCases = Clusterify(Cases, SI);

```

标题名,可能有些唬人,其实CaseVector Cases里面是存储是,switch指令里面case所对应的CaseRange信息

## 1.3实际应用场景

假设我们有以下 `Switch` 代码:

```c

switch (x) {

    case 1: ... break;

    case 2: ... break;

    case 100: ... break;

    default: ... break;

}

```

在 LowerSwitch 的转换过程中:

LazyValueInfo 会分析 x 的值范围,发现 x 的可能值为 1、2 或 100。

AssumptionCache 会检查是否有关于 x 的假设,例如“x 不可能为负数”。

基于这些信息,LowerSwitch 将 Switch 转换为一系列 Branch 指令,同时省略不必要的分支,例如负数的 case。

llvm进阶(2)LowerSwitch(Switch 到 Branch 的奥秘)

这里说明下这个CaseRange是什么东西?

举个例子:

llvm进阶(2)LowerSwitch(Switch 到 Branch 的奥秘)

这种情况下对应的CaseRange结构可能如下:

```

CaseRange(ConstantInt::get(/* value = */ 1), ConstantInt::get(/* value = */ 1), BB1);

CaseRange(ConstantInt::get(/* value = */ 2), ConstantInt::get(/* value = */ 4), BB2);

CaseRange(ConstantInt::get(/* value = */ 5), ConstantInt::get(/* value = */ 5), BB3);

```

### 3.2.1Clusterify 函数做了什么

将 `switch` 分支按连续值合并为簇,排除默认分支,并统计非默认分支数量。

```c

unsigned Clusterify(CaseVector &Cases, SwitchInst *SI) {

//初始化计数器,用于统计不引用默认分支的分支数量

unsigned NumSimpleCases = 0;

// 开始处理 "简单" 分支(即不引用默认分支的分支)

for (auto Case : SI->cases()) {

// 如果当前分支的目标基本块是默认分支,则跳过该分支

if (Case.getCaseSuccessor() == SI->getDefaultDest())

continue;

// 将当前分支转换为 CaseRange 并加入 Cases 列表

// CaseRange 的 Low 和 High 值初始化为当前分支的值(因为这是一个单一值的分支)

Cases.push_back(CaseRange(Case.getCaseValue(), Case.getCaseValue(),

Case.getCaseSuccessor()));

// 增加不引用默认分支的分支数量

++NumSimpleCases;

}

//对case进行排序

llvm::sort(Cases, CaseCmp());

// Merge case into clusters

// 如果 Cases 列表中有两个或更多分支,尝试将相邻的分支合并为簇

if (Cases.size() >= 2) {

// 定义迭代器 I,指向 Cases 列表的第一个分支

CaseItr I = Cases.begin();

// 定义迭代器 J,指向 Cases 列表的第二个分支,E 指向 Cases 列表的末尾

for (CaseItr J = std::next(I), E = Cases.end(); J != E; ++J) {

// 获取 J 分支的最小值(下界)

int64_t nextValue = J->Low->getSExtValue();

// 获取 I 分支的最大值(上界)

int64_t currentValue = I->High->getSExtValue();

// 获取 J 分支的目标基本块

BasicBlock* nextBB = J->BB;

BasicBlock* currentBB = I->BB;

// If the two neighboring cases go to the same destination, merge them

// into a single case.

// 如果相邻的两个分支满足以下条件:

// 1. J 分支的最小值等于 I 分支的最大值加 1(即连续的值)

// 2. 两个分支的目标基本块相同

// 则将这两个分支合并为一个簇

assert(nextValue > currentValue && "Cases should be strictly ascending");

if ((nextValue == currentValue + 1) && (currentBB == nextBB)) {

// 将 I 分支的上界更新为 J 分支的上界

I->High = J->High;

// FIXME: Combine branch weights.

// FIXME: 这里可以合并分支权重(未实现)

} else if (++I != J) {

// 如果分支不能合并,且 I 和 J 不指向同一个分支

// 则将 J 分支的值复制到 I 分支

*I = *J;

}

}

// 删除 Cases 列表中 I 之后的所有分支,因为它们已经被合并或复制

Cases.erase(std::next(I), Cases.end());

}

return NumSimpleCases;

}

```

接下来详细举例两个例子理解

llvm进阶(2)LowerSwitch(Switch 到 Branch 的奥秘)

根据Clusterify函数的处理逻辑

①遍历case 1-case4,并且增加到Cases vector中。

②根据case里面值进行排序。

③因为case的值有4个,那么开始遍历,因为case1的值为1,case2的值为2,但是他们的里面的block 分别为printf("%d",score)和printf("%d",score+1),所以并不能合成一个簇,换句话说 如果

llvm进阶(2)LowerSwitch(Switch 到 Branch 的奥秘)

case1,和case2共同拥有printf,且前一个数值是后者的一个值+1,那么就会把这两个case合并成一个。

本质上clusterify完成了一个合并case节点的功能,这个是编译器优化的一个步骤。

3.3 所有分支都跳转到默认分支的处理

根据3.2获得的case,如果case为空,说明switch的指令都跳转的default了。

代码完成的目的:判断switch指令所在的块没有case值,那么直接创建一个无条件跳转指令,从switch块直接跳转到default,由于变更了块与块之前的前置关系,所以要修复下phi指令。

```c

if (Cases.empty()) {

//创建一个无条件跳转到默认分支的指令

BranchInst::Create(Default, OrigBlock);

// Remove all the references from Default's PHIs to OrigBlock, but one.

// 修复默认分支的PHI节点,移除所有对OrigBlock的引用,但是要保留一个

FixPhis(Default, OrigBlock, OrigBlock);

//删除原始的SwitchInst指令

SI->eraseFromParent();

//返回结束处理

return;

}

```

## 3.4判断默认分支可达还是不可达

### 3.4.1默认分支不可达的定义

这里有两种

1.默认分支block块内,除phi或者调试指令的后续指令是UnreachableInst,那么这个就默认不可达。

2.根据case值的范围,判断默认分支可不可达。

第1种很容易看懂,这里着重分析第二种

```

const DataLayout &DL = F->getParent()->getDataLayout();

KnownBits Known = computeKnownBits(Val, DL, /*Depth=*/0, AC, SI);

ConstantRange KnownBitsRange =

ConstantRange::fromKnownBits(Known, /*IsSigned=*/false);

const ConstantRange LVIRange = LVI->getConstantRange(Val, SI);

ConstantRange ValRange = KnownBitsRange.intersectWith(LVIRange);

APInt Low = Cases.front().Low->getValue();

APInt High = Cases.back().High->getValue();

//计算 ValRange 的最小值、最大值与 Low、High 的最小值和最大值

APInt Min = APIntOps::smin(ValRange.getSignedMin(), Low);

APInt Max = APIntOps::smax(ValRange.getSignedMax(), High);

```

通过结合 已知位信息(KnownBits)和 Lazy Value Info(LVI)的范围分析,推断出变量 `Val` 的可能取值范围(`ValRange`),然后与给定的 `Cases` 中的 `Low` 和 `High` 值进行比较,计算出一个更精确的最小值(`Min`)和最大值(`Max`),最终目的是优化 `Val` 的范围。

得到范围之后

```

DefaultIsUnreachableFromSwitch = (Min + (NumSimpleCases - 1) == Max);

```

来判断默认分支是否可达,

举个例子好理解这句话:

```

// switch (val) {

// case 1: ... break;

// case 2: ... break;

// case 3: ... break;

// }

```

假如在上面的代码中得到的min是1,max是3,numsimpleCase是3,那么 1+3-1equal3,就判断出来 默认分支不可达,否则就存在可达的可能性。

## 3.5处理默认分支不可达的情形

## 3.6处理默认分支可达的情形

下方代码,首先创建了一个NewDefault的快,将新创建的块放到default之前,并且将newDefault直接指向她

```

BasicBlock *NewDefault = BasicBlock::Create(SI->getContext(), "NewDefault");

F->getBasicBlockList().insert(Default->getIterator(), NewDefault);

BranchInst::Create(Default, NewDefault);

```

## 3.7 将switch2if-then

创建将switch语句转换为if-then的层次结构

```

// 调用 SwitchConvert 函数,将 switch 语句转换为 if-then 语句的层次结构

// 参数说明:

// - Cases.begin(), Cases.end(): 所有 case 范围的迭代器

// - LowerBound, UpperBound: switch 语句的值范围

// - Val: switch 语句的条件值

// - OrigBlock: 原始的 switch 语句所在的基本块

// - NewDefault: 新的默认块

// - UnreachableRanges: 不可达范围列表

BasicBlock *SwitchBlock =

SwitchConvert(Cases.begin(), Cases.end(), LowerBound, UpperBound, Val,

OrigBlock, OrigBlock, NewDefault, UnreachableRanges);

```

### 3.7.1switchConver函数解析

#### 37.1.1size为1时

首先计算需要处理case的size大小,如果为1的情况下,检查case 范围是否正好被 LowerBound 和UpperBound 完全包围,如果被包围,确定当前 `case` 范围包含多少个值,然后去修复phi指令

```c

if (Size == 1) {

// 检查当前 case 范围是否正好被 LowerBound 和 UpperBound 完全包围

if (Begin->Low == LowerBound && Begin->High == UpperBound) {

// 计算当前 case 范围的大小

unsigned NumMergedCases = 0;

NumMergedCases = UpperBound->getSExtValue() - LowerBound->getSExtValue();

// 修复 PHI 节点,确保控制流正确

FixPhis(Begin->BB, OrigBlock, Predecessor, NumMergedCases);

// 返回当前 case 范围的目标基本块

return Begin->BB;

}

return NewLeafBlock(*Begin, Val, LowerBound, UpperBound, OrigBlock,

Default);

}

```

首先看(Begin->Low == LowerBound && Begin->High == UpperBound)条件满足情况下,算出NumMergedCases(有几个节点被合并了) 的值,然后FixPhis(Begin->BB, OrigBlock, Predecessor, NumMergedCases)

首先为什么会产生修复phis,

重点看

```

NumMergedCases = UpperBound->getSExtValue() - LowerBound->getSExtValue();

// 修复 PHI 节点,确保控制流正确

FixPhis(Begin->BB, OrigBlock, Predecessor, NumMergedCases);

```

这里我画出来图(着重讲fixPhis,这个其他的函数也会使用fixPhis):

```

1.功能:将在succBB第一次出现phi节点且输入值是origBB的节点的phi指令中的Incoming替换为newBB

1.1如果Switch语句都具有相同的值,则它们可能有多个输出边进入同一BB。转换switch语句时,这些传入边现在来自多个BB。

1.2如果后续传入值现在共享相同的情况,即多个结果边被压缩为一个,则删除,这对于保持phi值的数量等于SuccBB的分支数量是必要的。

```

大概操作如图所示:把在succBB中的phi指令,如果这个phi是orginB作为输入的,替换为newBB,同时后续除第一条来自orginBB的条指令都removeIncomingValue掉。

llvm进阶(2)LowerSwitch(Switch 到 Branch 的奥秘)

#### 3.7.1.2当size大于2时

按照查找二叉树的方式构建switch的case作为一个查找二叉树的方式进行构建。

llvm进阶(2)LowerSwitch(Switch 到 Branch 的奥秘)

```

if (Leaf.Low == Leaf.High)

{

// Make the seteq instruction...

Comp =

new ICmpInst(*NewLeaf, ICmpInst::ICMP_EQ, Val, Leaf.Low, "SwitchLeaf");

}

else

{

// Make range comparison

if (Leaf.Low == LowerBound)

{

// Val >= Min && Val <= Hi --> Val <= Hi

Comp = new ICmpInst(*NewLeaf, ICmpInst::ICMP_SLE, Val, Leaf.High,

"SwitchLeaf");

}

else if (Leaf.High == UpperBound)

{

// Val <= Max && Val >= Lo --> Val >= Lo

Comp = new ICmpInst(*NewLeaf, ICmpInst::ICMP_SGE, Val, Leaf.Low,

"SwitchLeaf");

}

else if (Leaf.Low->isZero())

{

// Val >= 0 && Val <= Hi --> Val <=u Hi

Comp = new ICmpInst(*NewLeaf, ICmpInst::ICMP_ULE, Val, Leaf.High,

"SwitchLeaf");

}

else

{

// Emit V-Lo <=u Hi-Lo

Constant *NegLo = ConstantExpr::getNeg(Leaf.Low);

Instruction *Add = BinaryOperator::CreateAdd(

Val, NegLo, Val->getName() + ".off", NewLeaf);

Constant *UpperBound = ConstantExpr::getAdd(NegLo, Leaf.High);

Comp = new ICmpInst(*NewLeaf, ICmpInst::ICMP_ULE, Add, UpperBound,

"SwitchLeaf");

}

}

```

通过一些跳转替换了switch语句,从而达到了目的。

例如代码如图所示,则处理为

llvm进阶(2)LowerSwitch(Switch 到 Branch 的奥秘)

详见如图所示。

这篇文章总结:swtich转化为其他的branch指令进行实现。

代码太多了,光看这篇文章展现不出来奥妙,后面录一个视频。

下面两幅图为switch转化为branch的对比图

llvm进阶(2)LowerSwitch(Switch 到 Branch 的奥秘)
llvm进阶(2)LowerSwitch(Switch 到 Branch 的奥秘)

学习逆向与爬虫,可以关注我朋友:

原文始发于微信公众号(二进制科学):llvm进阶(2)LowerSwitch(Switch 到 Branch 的奥秘)

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年4月6日19:09:55
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   llvm进阶(2)LowerSwitch(Switch 到 Branch 的奥秘)https://cn-sec.com/archives/3920686.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息