跳到内容
Shiny's Blog

循环

上一章学了 switch 多路分支的汇编形态。这一章学循环:C 里写 forwhiledo-while,编译器翻译成什么。

和前几章一样,编译 Debug x86,用 x64dbg 断到函数对照。汇编只保留循环结构相关的核心指令,过滤掉 Debug 噪音(每步读写栈、临时变量复制等,前面讲过)。

for 循环

for 是结构最完整的循环,有独立的初始化、条件检查、循环体、递增四个部分。先看 C 代码:

int sum_to(int n) {
    int total = 0;
    for (int i = 1; i <= n; i++) {
        total += i;
    }
    return total;
}

核心汇编:

mov  dword ptr [ebp-4], 0       ; total = 0
mov  dword ptr [ebp-14], 1     ; i = 1(初始化)
jmp  check                      ; ← 关键:先跳到条件检查
increment:
mov  eax, dword ptr [ebp-14]   ; i++
add  eax, 1
mov  dword ptr [ebp-14], eax
check:
mov  eax, dword ptr [ebp-14]   ; eax = i
cmp  eax, dword ptr [ebp+8]     ; i <= n ?
jg   end                        ; i > n → 跳出循环
mov  eax, dword ptr [ebp-4]     ; eax = total(循环体)
add  eax, dword ptr [ebp-14]   ; eax += i
mov  dword ptr [ebp-4], eax     ; total = eax
jmp  increment                  ; 循环体末尾跳回递增
end:
mov  eax, dword ptr [ebp-4]     ; 返回 total

for 循环在汇编里的固定结构:

for 循环汇编结构 (实际内存布局)

注意实际布局中 incrementcheck 之前,loop_bodycheck 之后。条件检查通过后 fall-through 进入循环体,循环体末尾 jmp increment 跳回递增,递增后 fall-through 到 check,形成环。初始化之后的那条 jmp check 是 for 循环最明显的标志,因为 for 的语义是”先检查条件,再决定是否执行循环体”,第一轮要先跳到 check,不能直接进循环体。

为什么先 jmp 到 check

for 的语义是”条件满足才执行循环体”。如果初始化后直接进循环体,那条件不满足时第一轮也会执行,这就变成 do-while 了。所以编译器在初始化后插一条 jmp check,先做条件检查,通过才 fall-through 进入循环体。

while 循环

while 和 for 都是”先检查后执行”,但 MSVC 生成的结构不同,while 没有独立的递增部分,所以不需要 jmp check 前置跳转:

int count_bits(int n) {
    int count = 0;
    while (n != 0) {
        count += n & 1;
        n >>= 1;
    }
    return count;
}

核心汇编:

mov  dword ptr [ebp-4], 0       ; count = 0
check:
cmp  dword ptr [ebp+8], 0       ; n != 0 ?
je   end                        ; n == 0 → 跳出循环
mov  eax, dword ptr [ebp+8]     ; eax = n
and  eax, 1                     ; n & 1
add  eax, dword ptr [ebp-4]     ; eax += count
mov  dword ptr [ebp-4], eax     ; count = eax
mov  eax, dword ptr [ebp+8]     ; eax = n
sar  eax, 1                     ; n >>= 1(算术右移)
mov  dword ptr [ebp+8], eax     ; n = eax
jmp  check                      ; 跳回检查
end:
mov  eax, dword ptr [ebp-4]     ; 返回 count

结构:

while 循环汇编结构 (无前置 jmp)

while 的结构和 for 不同,没有初始化后的 jmp check 前置跳转。因为 while 没有独立的递增部分,编译器直接把条件检查放在循环入口,je end 跳出,循环体末尾 jmp check 跳回。相比之下 for 有独立的递增部分(i++),递增在 check 之前,所以需要先 jmp check 跳过递增直达检查。

while 和 for 的结构差异

虽然都是”先检查后执行”,但 MSVC 生成的结构不同:

  • for:初始化 → jmp check → increment → check → loop_body → jmp increment
  • while:check → loop_body → jmp check

原因是 for 有独立的递增(i++),必须放在 check 之前但又不能第一轮就执行,所以需要 jmp check 跳过它。while 没有递增部分,直接 cmp + je 就行。

逆向时靠循环体末尾的跳转目标和递增操作来区分 for 和 while:循环末尾有”取出变量→加 1→存回”这种纯计数操作、且跳到 check 之前的多半是 for;循环体只有业务逻辑、末尾直接跳回 check 的多半是 while。 但这只是经验,不是绝对,编译器不关心你写的是哪种,只关心逻辑。

do-while 循环

do-while 是唯一一个先执行后检查的循环。循环体至少执行一次:

int gcd(int a, int b) {
    int temp;
    do {
        temp = a % b;
        a = b;
        b = temp;
    } while (b != 0);
    return a;
}

核心汇编:

loop_body:                      ; ← 直接进入循环体,没有 jmp check
mov  eax, dword ptr [ebp+8]     ; eax = a
cdq                             ; 扩展符号位到 edx(为 idiv 准备)
idiv eax, dword ptr [ebp+C]   ; eax / b,余数在 edx
mov  dword ptr [ebp-4], edx     ; temp = a % b
mov  eax, dword ptr [ebp+C]   ; eax = b
mov  dword ptr [ebp+8], eax     ; a = b
mov  eax, dword ptr [ebp-4]     ; eax = temp
mov  dword ptr [ebp+C], eax   ; b = temp
cmp  dword ptr [ebp+C], 0     ; b != 0 ?
jne  loop_body                  ; 不等于 0 → 继续
mov  eax, dword ptr [ebp+8]     ; 返回 a

结构:

do-while 循环汇编结构 (末尾检查)

do-while 最显著的特征:没有条件检查的前置跳转。 循环代码从 loop_body 标签开始直接执行,条件检查在末尾。这是和 for/while 的关键区别,for/while 是”先检查后执行”,条件检查在循环体之前;do-while 是”先执行后检查”,直接进循环体,末尾 jne 跳回。

这也解释了为什么 for/while 的条件跳转需要一个 end 标签,而 do-while 不需要。for/while 在循环体之前检查,检查失败时要跳到循环体之后,必须有 end 标记目标。do-while 在循环体之后检查,检查失败时不跳,fall-through 自然到了循环体后面,不需要专门的退出标签。

三种循环的对比如下:

特征forwhiledo-while
进入方式jmp check 跳过递增直达检查直接 cmp + je end直接进入循环体
条件检查位置循环体之前循环体之前循环体之后
最少执行次数0 次0 次1 次
递增部分有独立 i++
汇编标志初始化后有 jmp check没有 jmp,直接 cmp没有 jmp,直接进循环体
Release 下 do-while 特征不一定可靠

Release 模式下,编译器如果能证明循环至少执行一次,会把 for 和 while 也优化成 do-while 的形式(条件检查移到循环末尾,省掉一次 jmp)。所以逆向 Release 版本时,“没有前置 jmp”不能作为判定 do-while 的依据,你只能确定”这是一个循环”,具体是哪种语法要看循环体逻辑。

三种循环的 Release 形态

Release(/O2)下编译器会做循环展开、指令调度等优化,Debug 的结构差异被抹平。看两个函数的对比:

int sum_for(int n) {
    int s = 0;
    for (int i = 1; i <= n; i++) { s += i; }
    return s;
}

int sum_dowhile(int n) {
    int s = 0, i = 1;
    if (n >= 1) {
        do { s += i; i++; } while (i <= n);
    }
    return s;
}

MSVC 32 位 Release 编译(/O2),两个函数的汇编不一样

; sum_for — 编译器做了循环展开(每次加 2)
00441040  xor   edx, edx              ; s = 0
00441042  push  esi
00441043  xor   esi, esi
00441045  push  edi
00441046  lea   eax, [edx+1]          ; i = 1
00441049  cmp   ecx, 2                ; n < 2 ?
0044104C  jl    sum_while+1Dh
0044104E  lea   edi, [ecx-1]          ; n - 1
00441051  inc   esi
00441052  add   edx, eax              ; s += i
00441054  add   esi, eax
00441056  add   eax, 2                ; i += 2(循环展开)
00441059  cmp   eax, edi
0044105B  jle   sum_while+11h
0044105D  xor   edi, edi
0044105F  cmp   eax, ecx
00441061  cmovg eax, edi
00441064  add   eax, esi
00441066  pop   edi
00441067  add   eax, edx
00441069  pop   esi
0044106A  ret

; sum_dowhile — 没有循环展开,结构简单
00441070  xor   eax, eax              ; s = 0
00441072  lea   edx, [eax+1]          ; i = 1
00441075  cmp   ecx, edx              ; n >= 1 ?
00441077  jl    sum_dowhile+17h
00441079  nop   dword ptr [eax]
00441080  add   eax, edx              ; s += i
00441082  inc   edx                   ; i++
00441083  cmp   edx, ecx              ; i <= n ?
00441085  jle   sum_dowhile+10h
00441087  ret

sum_for 被循环展开(每次加 2),用了 lea 替代 movcmov 替代分支;sum_dowhile 没有展开,保持简单的 add+inc+cmp+jle不同循环写法在 Release 下生成不同代码,不要假设它们会统一。

逆向结论

在 Release 版本中,编译器会做循环展开、指令调度、寄存器分配等优化,Debug 下的结构差异(jmp check、递增段位置等)会被抹平。你无法区分原始代码用的是 for、while 还是 do-while,只能还原出”这是一个循环,循环条件是什么,循环体做了什么”。不要试图在 Release 里用 Debug 的结构特征去区分循环类型。

死循环

死循环是条件恒为真的循环:while(1)for(;;)do {...} while(1)。三种都能写,但 MSVC Debug(/Od)对它们的处理不同:

while (1) { printf("%d", n); }
for (;;)  { printf("%d", n); }
do        { printf("%d", n); } while (1);

核心汇编:

; while (1) — 开头保留 mov + test + je
check:
mov   eax, 1                     ; 加载常量 1
test  eax, eax                   ; 测试是否为零
je    end                        ; 为零则跳出(永远不跳)
mov   eax, dword ptr [n]         ; printf 参数
push  eax
push  offset "%d"
call  printf
add   esp, 8
jmp   check                      ; 跳回检查

; for (;;) — 完全没有条件检查
loop_body:
mov   eax, dword ptr [n]         ; printf 参数
push  eax
push  offset "%d"
call  printf
add   esp, 8
jmp   loop_body                  ; 直接无条件跳回

; do {} while (1) — 末尾保留 mov + test + jne
loop_body:
mov   eax, dword ptr [n]         ; printf 参数
push  eax
push  offset "%d"
call  printf
add   esp, 8
mov   eax, 1
test  eax, eax
jne   loop_body                  ; 非零则继续(永远跳)

三种写法的区别:

  • while(1):开头 mov eax,1 + test + je(永远不跳),循环体末尾 jmp 跳回开头
  • for(;;):完全省略条件检查,只有循环体 + 末尾 jmp 跳回
  • do {} while(1):末尾 mov eax,1 + test + jne(永远跳),没有开头的检查

while(1)jedo {} while(1)jne 测试的都是常量 1,一个永远不跳、一个永远跳,但 Debug 模式不优化,照常生成。for(;;) 语法上没有条件表达式,编译器直接省掉。

Release 下三种写法完全一样

Release(/O2)下编译器识别出常量条件,三种写法都省掉 test,变成纯粹的无条件 jmp 跳回,完全无法区分:

loop_body:
mov   eax, dword ptr [n]        ; printf 参数
push  eax
push  offset "%d"
call  printf
add   esp, 8
jmp   loop_body                 ; 三种写法都是这一条

识别死循环的特征:循环末尾是无条件 jmp 跳回(没有 cmp/test),或者有 test 但测试的是非零常量(je/jne 永远不跳/永远跳)。跳出只能靠 break(循环体内的条件 jmp 到循环之后)。

break 和 continue

break 和 continue 在汇编里都是 jmp,区别在于跳转目标不同:

int first_negative(int* arr, int len) {
    for (int i = 0; i < len; i++) {
        if (arr[i] < 0) {
            break;    // 跳出整个循环
        }
    }
    return 0;
}

int sum_positive(int* arr, int len) {
    int total = 0;
    for (int i = 0; i < len; i++) {
        if (arr[i] < 0) {
            continue; // 跳过本轮,继续下一轮
        }
        total += arr[i];
    }
    return total;
}

first_negative(带 break)的核心汇编:

mov  dword ptr [ebp-8], 0       ; i = 0
jmp  check
increment:
mov  eax, dword ptr [ebp-8]     ; i++
add  eax, 1
mov  dword ptr [ebp-8], eax
check:
mov  eax, dword ptr [ebp-8]     ; eax = i
cmp  eax, dword ptr [ebp+C]   ; i < len ?
jge  after_loop                 ; i >= len → 循环结束
mov  eax, dword ptr [ebp-8]     ; eax = i
mov  ecx, dword ptr [ebp+8]     ; ecx = arr
cmp  dword ptr [ecx+eax*4], 0   ; arr[i] < 0 ?(直接在内存比较)
jge  increment                  ; 大于等于 0 → 不 break,跳到递增
jmp  after_loop                 ; break!跳到循环之后
after_loop:                     ; ← break 跳到这里
xor  eax, eax                   ; return 0
arr[i] 的寻址公式

cmp dword ptr [ecx+eax*4], 0 就是拿 arr[i] 直接和 0 比较。ecx 是数组首地址(base),eax 是下标 i(index),*4 是因为 int 占 4 字节。通用公式是 [base + index * scale],scale 由元素大小决定。寻址模式的细节留到 c-asm-6 数组章节展开。

sum_positive(带 continue)的核心汇编:

mov  dword ptr [ebp-4], 0       ; total = 0
mov  dword ptr [ebp-14], 0     ; i = 0
jmp  check
increment:
mov  eax, dword ptr [ebp-14]   ; i++
add  eax, 1
mov  dword ptr [ebp-14], eax
check:
mov  eax, dword ptr [ebp-14]   ; eax = i
cmp  eax, dword ptr [ebp+C]   ; i < len ?
jge  end                        ; i >= len → 循环结束
mov  eax, dword ptr [ebp-14]   ; eax = i
mov  ecx, dword ptr [ebp+8]     ; ecx = arr
cmp  dword ptr [ecx+eax*4], 0   ; arr[i] < 0 ?(直接在内存比较)
jge  skip_continue              ; 大于等于 0 → 不 continue
jmp  increment                  ; continue!跳到递增部分
skip_continue:
mov  eax, dword ptr [ebp-14]   ; eax = i
mov  ecx, dword ptr [ebp+8]     ; ecx = arr
mov  edx, dword ptr [ebp-4]     ; edx = total
add  edx, dword ptr [ecx+eax*4] ; edx += arr[i]
mov  dword ptr [ebp-4], edx     ; total = edx
jmp  increment                  ; 执行完跳到递增
end:
mov  eax, dword ptr [ebp-4]     ; return total

关键区别在 jmp 的目标:

  • breakjmp after_loop — 跳过整个循环,到循环之后的代码
  • continuejmp increment — 跳过循环体剩余部分,但到递增部分,继续下一轮

逆向时看 jmp 的目标地址就能区分:跳到循环结构之后的是 break,跳到循环体内的递增标签的是 continue。

嵌套循环

两层循环就是两套 cmp + jxx + jmp 结构嵌套在一起。看冒泡排序:

void bubble_sort(int* arr, int n) {
    for (int i = 0; i < n - 1; i++) {
        for (int j = 0; j < n - 1 - i; j++) {
            if (arr[j] > arr[j + 1]) {
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

核心汇编:

mov  dword ptr [ebp-8], 0        ; i = 0(外层初始化)
jmp  outer_check
outer_increment:
mov  eax, dword ptr [ebp-8]      ; i++
add  eax, 1
mov  dword ptr [ebp-8], eax
outer_check:
mov  eax, dword ptr [ebp+C]    ; eax = n
sub  eax, 1                      ; n - 1
cmp  dword ptr [ebp-8], eax      ; i < n - 1 ?
jge  end                         ; i >= n-1 → 跳出外层
mov  dword ptr [ebp-14], 0      ; j = 0(内层初始化)
jmp  inner_check
inner_increment:
mov  eax, dword ptr [ebp-14]    ; j++
add  eax, 1
mov  dword ptr [ebp-14], eax
inner_check:
mov  eax, dword ptr [ebp+C]    ; eax = n
sub  eax, 1                      ; n - 1
sub  eax, dword ptr [ebp-8]      ; n - 1 - i
cmp  dword ptr [ebp-14], eax    ; j < n - 1 - i ?
jge  outer_increment             ; j >= limit → 跳回外层递增
; ── if (arr[j] > arr[j+1]) 则 swap ──
mov  eax, dword ptr [ebp-14]    ; eax = j
mov  ecx, dword ptr [ebp+8]      ; ecx = arr
mov  edx, dword ptr [ebp-14]    ; edx = j
mov  esi, dword ptr [ebp+8]      ; esi = arr
mov  eax, dword ptr [ecx+eax*4]  ; eax = arr[j]
cmp  eax, dword ptr [esi+edx*4+4]; arr[j] > arr[j+1] ?
jle  inner_increment             ; 不大于 → 跳过 swap
; swap: temp = arr[j]
mov  eax, dword ptr [ebp-14]    ; eax = j
mov  ecx, dword ptr [ebp+8]      ; ecx = arr
mov  edx, dword ptr [ecx+eax*4]  ; edx = arr[j]
mov  dword ptr [ebp-20], edx    ; temp = arr[j]
; arr[j] = arr[j+1]
mov  eax, dword ptr [ebp-14]    ; eax = j
mov  ecx, dword ptr [ebp+8]      ; ecx = arr
mov  edx, dword ptr [ebp-14]    ; edx = j
mov  esi, dword ptr [ebp+8]      ; esi = arr
mov  edx, dword ptr [esi+edx*4+4]; edx = arr[j+1]
mov  dword ptr [ecx+eax*4], edx  ; arr[j] = arr[j+1]
; arr[j+1] = temp
mov  eax, dword ptr [ebp-14]    ; eax = j
mov  ecx, dword ptr [ebp+8]      ; ecx = arr
mov  edx, dword ptr [ebp-20]    ; edx = temp
mov  dword ptr [ecx+eax*4+4], edx; arr[j+1] = temp
jmp  inner_increment             ; 跳回内层递增
end:
Debug 重复加载同一个变量

swap 部分每次用 arrj 都重新从栈上 mov 出来(mov ecx, dword ptr [ebp+8] 出现了 5 次)。这是 Debug(/Od)的机械行为,每步都从栈读写,寄存器只做临时打工。Release 会把 arrj 固定在寄存器里,这些重复加载全部消失。

嵌套循环的结构:

嵌套循环汇编结构 (两套 jmp check + cmp + jxx)

识别嵌套循环的关键:找两对 jmp check + cmp + jge 结构。内层循环的跳转目标都在内层范围内(inner_incrementinner_check → 循环体 → inner_increment),外层循环的跳转目标跨越整个内层结构(outer_check 包含完整的内层循环,inner_check 失败时跳到 outer_increment)。

Release 下寄存器分配让结构更清晰

Debug 版所有变量都在栈上([ebp-8][ebp-14]),两套循环结构混在密集的栈读写里不好认。逆向嵌套循环时优先看 Release 版,寄存器分配后结构一目了然:esi = 外层 i,ebx = 内层 j,两套 inc + cmp + jge 结构清晰可见。

逆向识别清单

特征含义
初始化后 jmp check + 有递增段for(先检查后执行,有递增)
直接 cmp + je end,无递增while(先检查后执行,无递增)
没有 jmp,直接进循环体do-while(先执行后检查)
jmp 到循环之后break
jmp 到递增/检查标签continue
两对 jmp check + cmp + jxx嵌套循环(两层)

for 的特征是初始化后的 jmp check + 独立递增段while 的特征是直接 cmp + je end 无递增段do-while 的特征是直接进循环体、末尾 jne 跳回。但 Release 模式下编译器可能把 for/while 优化成 do-while 形式(省掉前置跳转),所以 Release 版本的”没有 jmp”不能作为 do-while 的判定依据。

练习

  1. 下面这段汇编实现了一个什么循环?还原出 C 代码。

    mov  dword ptr [ebp-4], 0       ; total = 0
    mov  dword ptr [ebp-8], 1       ; i = 1
    jmp  check
    increment:
    mov  eax, dword ptr [ebp-8]     ; i++
    add  eax, 1
    mov  dword ptr [ebp-8], eax
    check:
    cmp  dword ptr [ebp-8], 10      ; i <= 10 ?
    jg   end                        ; i > 10 → 跳出
    mov  eax, dword ptr [ebp-8]     ; eax = i
    imul eax, eax                   ; eax = i * i
    add  dword ptr [ebp-4], eax     ; total += i * i
    jmp  increment                  ; 跳回递增
    end:
    mov  eax, dword ptr [ebp-4]
    参考答案
    int sum_squares(void) {
        int total = 0;
        for (int i = 1; i <= 10; i++) {
            total += i * i;
        }
        return total;
    }

    初始化后有 jmp check + 独立递增段 → for 结构。imul eax, eaxi * icmp ..., 10 + jg end 取反得到 i <= 10

  2. 判断下面这段汇编是 for/while 还是 do-while?还原出 C 代码。

    mov  eax, dword ptr [ebp+8]     ; eax = n
    xor  ecx, ecx                   ; count = 0
    test eax, eax
    jle  done
    loop_body:
    test eax, 1
    jz   skip
    inc  ecx
    skip:
    sar  eax, 1
    test eax, eax
    jnz  loop_body
    done:
    mov  eax, ecx
    参考答案

    这是 do-while 结构(Release 优化后)。特征:进入前有一次 test eax, eax + jle done 做保护检查,之后直接进 loop_body,末尾 test eax, eax + jnz loop_body 跳回。没有前置的 jmp check

    int count_bits(int n) {
        int count = 0;
        if (n <= 0) return 0;
        do {
            if (n & 1) count++;
            n >>= 1;
        } while (n != 0);
        return count;
    }

    不过正如本章说的,Release 模式下编译器可能把 for/while 优化成 do-while 形式。原始代码也可能是 while (n != 0)for (; n != 0; n >>= 1),生成的 Release 汇编可能完全一样。

  3. 下面这段汇编包含嵌套循环。找出外层和内层循环的边界,说明循环变量和终止条件。

    mov  dword ptr [ebp-C], 0      ; sum = 0
    mov  dword ptr [ebp-4], 0       ; i = 0
    jmp  outer_check
    outer_increment:
    mov  eax, dword ptr [ebp-4]     ; i++
    add  eax, 1
    mov  dword ptr [ebp-4], eax
    outer_check:
    mov  ecx, dword ptr [ebp+8]     ; ecx = n
    cmp  dword ptr [ebp-4], ecx     ; i < n ?
    jge  end                        ; i >= n → 跳出外层
    mov  dword ptr [ebp-8], 0       ; j = 0(内层初始化)
    jmp  inner_check
    inner_increment:
    mov  eax, dword ptr [ebp-8]     ; j++
    add  eax, 1
    mov  dword ptr [ebp-8], eax
    inner_check:
    mov  eax, dword ptr [ebp+8]     ; eax = n
    cmp  dword ptr [ebp-8], eax     ; j < n ?
    jge  outer_increment            ; j >= n → 跳回外层递增
    mov  eax, dword ptr [ebp-4]     ; eax = i
    add  eax, dword ptr [ebp-8]     ; eax += j
    add  dword ptr [ebp-C], eax   ; sum += i + j
    jmp  inner_increment            ; 跳回内层递增
    end:
    mov  eax, dword ptr [ebp-C]   ; return sum
    参考答案

    两层嵌套循环:

    • 外层循环:变量 i[ebp-4]),从 0 开始,终止条件 i < n,递增 i++
    • 内层循环:变量 j[ebp-8]),从 0 开始,终止条件 j < n,递增 j++
    • 循环体:sum += i + j
    int func(int n) {
        int sum = 0;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                sum += i + j;
            }
        }
        return sum;
    }

    识别线索:两对 jmp check + cmp + jge end 结构,内层的 check 在外层的递增之前。

  4. 下面这段汇编里有 break 还是 continue?还原出 C 代码。

    mov  dword ptr [ebp-4], 0       ; i = 0
    jmp  check
    increment:
    mov  eax, dword ptr [ebp-4]     ; i++
    add  eax, 1
    mov  dword ptr [ebp-4], eax
    check:
    mov  ecx, dword ptr [ebp-4]
    cmp  ecx, dword ptr [ebp+C]   ; i < len ?
    jge  after_loop                 ; i >= len → 循环结束
    mov  eax, dword ptr [ebp+8]     ; eax = arr
    mov  ecx, dword ptr [ebp-4]     ; ecx = i
    mov  edx, dword ptr [eax+ecx*4] ; edx = arr[i]
    cmp  edx, 0                     ; arr[i] == 0 ?
    jne  increment                  ; 不等于 0 → 不 break,跳到递增
    after_loop:                     ; ← break 跳到这里
    mov  eax, dword ptr [ebp-4]     ; return i
    参考答案

    breakjne increment 不满足条件时跳到递增(继续循环),满足条件时 fall-through 到 after_loop(循环之后)。等价于 jmp after_loop,跳过整个循环。

    int first_zero(int* arr, int len) {
        int i;
        for (i = 0; i < len; i++) {
            if (arr[i] == 0) {
                break;
            }
        }
        return i;
    }

    jne increment 取反得到 arr[i] == 0。满足时 fall-through 到循环之后 → break。返回值是 i,所以函数找的是第一个 0 的下标。


目录