---
id: 04e03e046bb46ab34aaeeed79279923b
author: Lee Yunjin
title: Geração de código assembly AMD64 para condicionais (if) em Go e análise de instruções de desvio
description: O compilador Go traduz instruções condicionais if-else em instruções de desvio de máquina AMD64, como CMPQ, JGE e JMP, e compara esse processo com o resultado da geração de código de desvio do compilador C (-O2).
language: pt
date: 2026-05-17T12:00:00Z
path: /blog/posts/go-if-statement-analysis-3012d30c
category: lowlevel
---

## Instruções If na Linguagem Go

Primeiramente, a razão pela qual escolhemos Go é que, entre as linguagens modernas, ela é aquela cujo assembly é, por vezes, o mais "elegante", e a eficiência de sua sintaxe chega a ser esmagadora mesmo em comparação com linguagens clássicas.

Agora que, na aula anterior, compreendemos o modo de funcionamento de um programa simples em Go, vamos comparar Go e Assembly linha por linha.

### Código-fonte

Antes de tudo, assim como em Go, até mesmo compiladores modernos, incluindo o GCC, otimizam automaticamente desvios condicionais que não têm utilidade. Compiladores de C como GCC e Clang também realizam otimizações bastante agressivas com -O2, o padrão da indústria; assim, a época em que o programador dificilmente podia confiar totalmente no compilador terminou de vez no final do século XX.

Portanto, do ponto de vista do compilador, para que a condição faça sentido é preciso impor condições que ele não consiga prever facilmente nem substituir por outra construção.

```go
package main

import (
    "os"
    "strconv"
)

func main() {
    // Se isto for substituído por algo previsível e que permita remover o desvio, como x = 10,
    // o compilador otimiza e remove o desvio.
    // Por isso, para observar esse assembly diretamente, em C costuma-se usar -O0 ou,
    // o que é ainda mais simples, fazer o compilador utilizar um valor externo imprevisível,
    // mas como esta seção trata de programação moderna, não usaremos
    // o método de desativar a otimização binária do Go.
    if len(os.Args) < 2 {
        return
    }
    x, _ := strconv.Atoi(os.Args[1])

    if x < 10 {
        println("X is smaller than 10")
    } else {
        println("X is larger or same as 10")
    }
}
```

Neste caso, como o compilador não consegue prever a entrada, o desvio condicional é traduzido diretamente para linguagem de máquina.

### Assembly

```asm
TEXT main.main(SB) /home/yjlee/introduction-to-golang/learn-golang/if-and-switch/golang-if/main.go
  main.go:8             0x47a840                493b6610                CMPQ SP, 0x10(R14)
  main.go:8             0x47a844                7670                    JBE 0x47a8b6
  main.go:8             0x47a846                55                      PUSHQ BP
  main.go:8             0x47a847                4889e5                  MOVQ SP, BP
  main.go:8             0x47a84a                4883ec10                SUBQ $0x10, SP
  main.go:15            0x47a84e                48833d12fb0a0002        CMPQ os.Args+8(SB), $0x2
  main.go:15            0x47a856                7c58                    JL 0x47a8b0
  main.go:15            0x47a858                488b0d01fb0a00          MOVQ os.Args(SB), CX
  main.go:18            0x47a85f                488b4110                MOVQ 0x10(CX), AX
  main.go:18            0x47a863                488b5918                MOVQ 0x18(CX), BX
  main.go:18            0x47a867                e834e8ffff              CALL strconv.Atoi(SB)
  main.go:20            0x47a86c                4883f80a                CMPQ AX, $0xa
  main.go:20            0x47a870                7d1d                    JGE 0x47a88f
  main.go:21            0x47a872                e809befbff              CALL runtime.printlock(SB)
  main.go:21            0x47a877                488d0519f50100          LEAQ 0x1f519(IP), AX
  main.go:21            0x47a87e                bb15000000              MOVL $0x15, BX
  main.go:21            0x47a883                e878c6fbff              CALL runtime.printstring(SB)
  main.go:21            0x47a888                e853befbff              CALL runtime.printunlock(SB)
  main.go:21            0x47a88d                eb1b                    JMP 0x47a8aa
  main.go:23            0x47a88f                e8ecbdfbff              CALL runtime.printlock(SB)
  main.go:23            0x47a894                488d05c8040200          LEAQ 0x204c8(IP), AX
  main.go:23            0x47a89b                bb1a000000              MOVL $0x1a, BX
  main.go:23            0x47a8a0                e85bc6fbff              CALL runtime.printstring(SB)
  main.go:23            0x47a8a5                e836befbff              CALL runtime.printunlock(SB)
  main.go:25            0x47a8aa                4883c410                ADDQ $0x10, SP
  main.go:25            0x47a8ae                5d                      POPQ BP
  main.go:25            0x47a8af                c3                      RET
  main.go:16            0x47a8b0                4883c410                ADDQ $0x10, SP
  main.go:16            0x47a8b4                5d                      POPQ BP
  main.go:16            0x47a8b5                c3                      RET
  main.go:8             0x47a8b6                e845f0feff              CALL runtime.morestack_noctxt.abi0(SB)
  main.go:8             0x47a8bb                eb83                    JMP main.main(SB)
```

Usando o go tool, obtemos de forma gentil a correspondência de cada construção com cada instrução de assembly.

Como o que aprenderemos desta vez são as comparações e os desvios if, basta observar algumas linhas.

### Instruções CMPQ e JL

A instrução `CMPQ` é utilizada para comparar valores do tipo de 4 bytes (4 words), e seu étimo vem de **C**o**MP**are**Q**uadword, abreviado como **CMPQ**.

No endereço de memória `0x47a84e`, encontramos a construção `CMPQ os.Args+8(SB), $0x2`.
Neste caso, o programa compara o número de argumentos recebidos com o hexadecimal `0x2` (ou seja, simplesmente 2).

Em seguida, para verificar se o número de argumentos é menor que 2 (isto é, se o programa possui apenas a si mesmo como argumento), realiza-se um salto condicional por meio de `JL`. Ou seja, trata-se de **J**ump, abreviação de **L**ess than, resultando em **JL**.
Se, no resultado da comparação anterior, o número de argumentos for menor que 2, o salto ocorre para o endereço `0x47a8b0`, onde há um `JGE`.
Entretanto, esta construção utiliza o registrador `AX`, portanto é preciso saber qual é o valor armazenado nele.

### Instrução MOVQ

Na sequência, é preciso entender como o registrador 'CX' é usado para armazenar o endereço inicial dos dados e como se pretende extrair os dados reais após a leitura desse endereço.

Observando o intervalo `0x47858`-`0x47863`, vemos que essa operação é realizada em etapas.

Primeiro, o endereço inicial do array de argumentos é inserido no registrador CX pela instrução `MOVQ os.Args(SB), CX`. Nesse momento, é preciso compreender o tipo `string` do Go.

Em Go, `string` é uma struct, e essa struct é composta por dois dados de 8 bytes, totalizando 16 bytes.

| struct | 8 bytes         | 8 bytes              |
| ------ | --------------- | -------------------- |
| string | endereço de memória | comprimento da string |

Visualmente, a estrutura é como acima: os primeiros 8 bytes armazenam o endereço inicial da string, e os 8 bytes seguintes armazenam o comprimento da string.

Assim, o endereço da string é armazenado no registrador AX, e o comprimento da string no registrador BX.

### CALL

Nos posts anteriores, ao observarmos funções da família `runtime`, a instrução CALL aparecia junto delas.
Ela é utilizada antes das funções empregadas em Go e significa que uma função está sendo **chamada**, daí o nome CALL. Em seguida, a função CALL é utilizada para converter a string em um inteiro, mas onde esse inteiro é armazenado não fica visível, pois _fica abstraído dentro da função._

### CMPQ e JGE

Voltando ao endereço anterior `0x47a86c`, a instrução **compara o endereço da string com o número `0xa` (em decimal, 10)!**

Isso significa que, como o programa não utiliza mais aquele argumento, **a posição da string foi sobrescrita para criar o espaço da variável inteira `x`.**

Esta é a essência da otimização agressiva realizada em linguagens como Go.

Depois, aparece a instrução JGE, abreviação de **J**ump, **G**reater or **E**quals. Portanto, esta construção pergunta se o valor é maior ou igual ao valor de comparação.

Assim, em vez de `x < 10` exatamente como escrito, a direção da comparação está invertida: **a ordem da comparação foi trocada!**
Isso ocorre porque, em linguagem de máquina, desviar antecipadamente quando a condição não é atendida é mais direto do que executar a comparação uma vez quando a condição é atendida e depois verificar novamente se ela não é atendida, e assim **economiza uma instrução**.

Esse tipo de otimização é bastante clássico; por isso, diferentemente do exemplo `strconv.Atoi` visto acima, ele aparece com frequência mesmo em compiladores com otimização bem mais modesta, valendo a pena conhecê-lo.

Assim, aplicando esse princípio, é possível obter um código-fonte diferente que resulta em um assembly 100% idêntico.

### Exemplo de código espelhado

Usando o script abaixo, o script bash gera dois códigos-fonte espelhados 100% e, em seguida, verifica que, desconsiderando os metadados que mudam a cada execução, ao considerar apenas `main`, obtém-se um assembly exatamente idêntico.

```bash
#!/usr/bin/env bash

# 1. Limpeza completa dos arquivos e diretórios residuais de execuções anteriores
echo "[1/6] Cleaning up old artifacts..."
rm -rf test_dir main_orig main_asm orig.asm asm.asm orig_pure.asm asm_pure.asm
mkdir -p test_dir

# 2. Escrita do código-fonte da versão original (main.go)
echo "[2/6] Generating main.go..."
cat << 'EOF' > main.go
package main

import (
        "os"
        "strconv"
)

func main() {
        if len(os.Args) < 2 {
                return
        }
        x, _ := strconv.Atoi(os.Args[1])

        s1 := "X is smaller than 10"
        s2 := "X is larger or same as 10"

        if x < 10 {
                println(s1)
        } else {
                println(s2)
        }
}
EOF

# 3. Escrita do código-fonte da versão espelhada (main_from_asm.go)
# Mantendo a estrutura simétrica dos operadores para que o compilador adote exatamente o mesmo padrão de otimização (JGE)
echo "[3/6] Generating main_from_asm.go..."
cat << 'EOF' > main_from_asm.go
package main

import (
        "os"
        "strconv"
)

func main() {
        if len(os.Args) < 2 {
                return
        }
        x, _ := strconv.Atoi(os.Args[1])

        s1 := "X is smaller than 10"
        s2 := "X is larger or same as 10"

        // Usando 10 como referência, se mantivermos a estrutura x < 10, o compilador
        // adota exatamente o mesmo mecanismo JGE e a mesma disposição de blocos de main.go
        switch {
        case x < 10:
                println(s1)
        default:
                println(s2)
        }
}
EOF

# 4. Compilação das duas fontes no mesmo caminho de diretório e com o mesmo nome de arquivo
echo "[4/6] Compiling both sources inside 'test_dir'..."
cp main.go test_dir/main.go
cd test_dir && go build -o ../main_orig main.go && cd ..
rm test_dir/main.go

cp main_from_asm.go test_dir/main.go
cd test_dir && go build -o ../main_asm main.go && cd ..

# 5. Extração da função assembly pura main.main usando go tool objdump
echo "[5/6] Extracting main.main assembly sections..."
go tool objdump -s "main\.main" main_orig > orig.asm
go tool objdump -s "main\.main" main_asm > asm.asm

# Remoção dos valores de endereço virtual, deslocamento e texto dos bytes de máquina,
# filtrando apenas os campos do conjunto de instruções puras (Opcode & Operands) que a CPU executa
awk '{print $4, $5, $6, $7}' orig.asm > orig_pure.asm
awk '{print $4, $5, $6, $7}' asm.asm > asm_pure.asm

# 6. Verificação da integridade estrutural das instruções via diff
echo "[6/6] Verifying assembly structural integrity via diff..."
echo "------------------------------------------------------------"
if diff orig_pure.asm asm_pure.asm > /dev/null; then
    echo "===> [성공] 두 바이너리의 main.main 기계어 로직이 100% 일치합니다! <==="
    echo "컴파일러의 최적화 파이프라인 가이드라인을 완벽하게 동기화하여 동일한 어셈블리를 얻었습니다."
else
    echo "===> [실패] 어셈블리 명령어 구조에 차이점이 발견되었습니다. <==="
    diff -u orig_pure.asm asm_pure.asm
fi
echo "------------------------------------------------------------"
```

Executando o código na prática, obtemos as informações abaixo.

```text
[1/6] Cleaning up old artifacts...
[2/6] Generating main.go...
[3/6] Generating main_from_asm.go...
[4/6] Compiling both sources inside 'test_dir'...
[5/6] Extracting main.main assembly sections...
[6/6] Verifying assembly structural integrity via diff...
------------------------------------------------------------
===> [성공] 두 바이너리의 main.main 기계어 로직이 100% 일치합니다! <===
컴파일러의 최적화 파이프라인 가이드라인을 완벽하게 동기화하여 동일한 어셈블리를 얻었습니다.
------------------------------------------------------------
```

### Conclusão

Linguagens de programação oferecem muitas abstrações, mas por trás delas escondem-se otimizações muito interessantes e agressivas. Além disso, explorando esse ponto ao contrário, também foi possível criar códigos espelhados, com fontes diferentes mas assembly idêntico. Se você se interessa por baixo nível e um dia encontrar software proprietário escrito em Go, desmontar o assembly e analisá-lo para recuperar o código-fonte não parece ser algo impossível.

## Próxima aula

Na próxima vez, veremos a instrução If e outra construção igualmente interessante, a instrução `select-case`.
