文章封面图

Essay2026 / 03 / 17

汇编入门-控制代码与汇编间的映射

本文以c语言与x86-64为基底,讲述控制语句的汇编实现。即if-else条件分支语句,a?b :c条件表达式,while循环,do-while循环,for循环,switch-case语句

if-else语句 & 条件表达式

最常见的C内条件分支语句的格式为

if(test-expr)
    then-statement
else
    else-statement

那么利用goto语句模拟汇编控制流,则转化为

    t = test-expr1;
    if(t)
        goto True;
    else-statement
    goto Done;
True:
    then-statement
Done:

这是使用 条件控制 来进行实现条件分支的操作,它简单通用,但是对于现代CPU而言是低效的。 对于简单的条件分支语句,有一种更加高效的实现方法,即 条件传送 。其思路为先计算条件分支的结果,然后再根据条件传送相应的结果。其高效程度在于无需条件跳转,使得CPU流水线填充更满,运行速度更快。

注:现代CPU具有分支预测能力以优化条件跳转,即在条件判断完成前,预先判断条件结果并执行后续指令。但是一旦预测失败则代表前面的活白干了。

下面为例子

long absdiff(long x,long y)
{
    long result;
    if(x > y) result = y-x;
    else result = x - y;
    return result;
}
条件分支实现(使用条件跳转)
x --> %rdi, y --> %rsi
absdiff
    comq %rsi,%rdi
    jg L1            程序执行的性能瓶颈处,必须知道条件结果后才能开始执行后续指令
    movq %rsi,%rax
    subq %rdi,%rax
    ret
L1:
    movq %rdi,%rax
    subq %rsi,%rax
    ret
条件传递
x --> %rdi , y --> % rsi 
absdiff:
    movq %rsi,%rax
    subq %rdi,%rax
    movq $rdi,%rdx
    subq %rsi,%rdx
    cmpq $rsi,%rdi
    cmovge %rdx,%rax  使用条件传送指令,即使条件结果没有出来,CPU流水线依旧可以执行下一条指令
    ret

当然,在C里面还有一个使用于条件传送,你可以试一试.即 a = test-expr ? then-expr : else-expr

do-while循环

其通用语句为

do
    body-statement
    while (test-expr);

翻译为接近汇编控制流的代码如下

Loop:
    body-statement
    v = text-expr
    if(v) goto Loop

下面以具体的c代码作为示范讲解.该代码实现阶乘的运算。你可以自己使用gcc将下面的c代码编译为汇编语言进行验证学习

long fact_do(long n){
    long result = 1;
    do{
            result *= n;
            n--;
    }while(n > 1);
    return result;
}
fact_do:                 注释
.LFB0:
    movl    $1, %eax     令result = 1
.L2:
    imulq   %rdi, %rax   result = result * n
    subq    $1, %rdi     n = n -1
    cmpq    %1, %rdi     n比较1
    jg  .L2              如果n>1,则跳转至L2
    rep ret

while循环

while循环的汇编实现有两种。一种是 jump-to-test形式 直观但效率低。另一种为 guarded-do形式 它是编译器常见的优化形式。 先以C代码模拟这两种汇编的控制流

//源码
while(test-expr)
    body-statement

//jump-tp-test形式
//while的直接汇编形式
    goto test
loop:
    body-statement
test:
    t = test-expr
    if(t) goto loop;

//guarded-do形式
//将while转变为do-while的形式后再汇编
    t = test-expr
    if(!t)
        goto done;
    loop:
        body-statement;
        t = test-expr;
        if(t) goto loop;
    done:

注:关于guarded-do为什么比jump-to-test好,因为二者在多次循环下没有差异,而在循环次数较少或者没有循环的情况下,guarded-do因为减少一次跳转操作而性能更高。除此之外,guarded-do形式无论进入循环与否,代码都是顺序执行的状态,而jmp-to-test则有开头跳转至中间的一步,这也带来一点点的性能差异

接下来以实际代码举例

long fact_while(unsigned long n)
{
    long result = 1;
    while(n > 1){
        result *= n;
        n--;
    }
    return result;
} 

本处为开启-Og优化级别的汇编代码,为jump-to-test实现

fact_while:             注释
.LFBO:
    movl    $1,%eax     result = 1
    jmp .L2             跳转至L2
.L3:                    循环体内容标签
    imulq   %rdi,%rax   result = result * n
    subq    $1,%rdi     n = n - 1;
.L2                     while条件验证标签
    cmpq    $1,%rdi     比较n与1
    ja  .L3             在n>1时跳转至L3
    rep ret

本处为开启-O1优化级别的汇编代码,实现为guarded-do

fact_while:             注释
.LFB0:
    cmpq    $1, %rdi    比较n与1
    jbe .L4             n<=1时,跳转至L4
    movl    $1, %eax    result = 1
.L3:
    imulq   %rdi, %rax  result = n * result
    subq    $1, %rdi    n = n - 1
    cmpq    $1, %rdi    比较n与1
    jne .L3             n!=1时,跳转至L3
    rep ret
.L4:
    movl    $1, %eax    result = 1
    ret

for循环

for循环翻译为汇编的思路为,for转变为while形式。然后按while的两种转变形式翻译为汇编代码

//源码
for(init-expr ; test-expr ; update-expr)
    body-statement;

//翻译为while形式
init-expr;
while(test-expr)
    body-statement;
    update-expr;

//再把while翻译到汇编形式
//下面为guarded-do形式
    init-expr;
    t = test-expr;
    if(!t) goto done;
loop:
    body-statement;
    update-expr;
    t = test-expr;
    if(t) goto loop;
done:

循环的continue与break

一个循环的级别汇编结构为,而break就是跳转到done标签,continue就是跳转至loop标签

loop:
    body-statement;
    break;          //等同于 goto done
    continue      //等同于 goto loop
    t = test-expr
    if(t) goto loop
done:

switch-case语句

switch-case语句使得我们可以根据一个索引值进行多重分支(multiway brancing) gcc将根据分支数量与密度来选择将其翻译为哪种形式 分支数量少 通常分支数量小于4的情况下,采用 条件分支链 来汇编

//源码
switch(index){
    case 1: body-1; 
    case 2: body-2; break;
    default: body-3;
    case 100: body-4;
}

//模拟汇编控制流代码
    if(index == 1) goto L1;
    if(index == 2)  goto L2;
    if(index == 100) goto L3;
    body-3;
L3:
    body-4;
    goto done;
L1:
    body-1
L2: 
    body-2;
    goto done;
done:

分支数量多,索引值间较为密集 此时采用 跳转表(jump table) 来实现。跳转表就是一个包含有跳转指令的目标

//源码
switch(index){
    case 100: body-1;
    case 101: body-2;break;
    case 103: body-3;break;
    case 110: body-4;
    default:  body-5;break;
}

//模拟汇编控制流代码
static void* jt[11] = {&&Loc_A , &&Loc_B , &&Loc_C
                       &&Loc_def,&&Loc_def,&&Loc_def,
                       &&Loc_def,&&Loc_def,&&Loc_def,
                       &&Loc_def,&&Loc_D}
unsigned t = index - 100;
if(t > 10) goto Loc_def;
goto *jt[t]

Loc_A: body-1
Loc_B: body-2; goto done;
Loc_C: body-3; goto done;
Loc_D: body-5;
Loc_def: body-4; goto done;

分支数量多,但是密度低 此时GCC通常采取 生成决策树策略。由于是根据索引查找跳转目标。那么利用查找算法的思想,如二分查找。先将case值排序,然后构建平衡二叉排序树,然后由此调整汇编

switch(case){
    case:-10 body-1;
    case:2   body-2;   
    case:10  body-3;
    case:100 body-4;
    case:114514 body-5;
    default: body-6;
}

//基本思路
if(case == 10) goto Loc_C;
if(case > 10) goto R_B;
L_B:
    if(case == 2) goto Loc_B;
    if(case == -1) goto Loc_A;
    goto Loc_def;
R_B:
    if(case == 100) goto Loc_D;
    if(case == 114514) goto Loc_E;
    goto Loc_def;
Loc_A: body-1;
Loc_B: body-2;
Loc_C: body-3;
Loc_D: body-4;
Loc_E: body-5;
Loc_def: body-6;