---
id: 04e03e046bb46ab34aaeeed79279923b
author: Lee Yunjin
title: Generazione del codice assembly AMD64 per le istruzioni condizionali (if) in Go e analisi delle istruzioni di salto
description: Questo testo confronta il processo mediante cui il compilatore Go traduce le istruzioni condizionali if-else in istruzioni di salto della macchina AMD64, come CMPQ, JGE e JMP, con il risultato della generazione del codice di diramazione prodotto dal compilatore C (-O2).
language: it
date: 2026-05-17T12:00:00Z
path: /blog/posts/go-if-statement-analysis-3012d30c
category: lowlevel
---

## Istruzioni If in Go

Innanzitutto, la ragione per cui abbiamo scelto Go è che, tra i linguaggi moderni, è quello il cui assembly risulta più "elegante", e che la sua sintassi è talvolta persino più efficiente di quella dei linguaggi classici.

Ora che, nella lezione precedente, abbiamo compreso il funzionamento di un semplice programma Go, confrontiamo Go e Assembly riga per riga.

### Codice sorgente

Anzitutto, come accade in Go, anche i moderni compilatori, GCC incluso, ottimizzano automaticamente i rami che non hanno alcuno scopo. Anche i compilatori del linguaggio C, come GCC e Clang, applicano ottimizzazioni molto aggressive già con -O2, lo standard del settore; pertanto, l'epoca in cui il programmatore difficilmente poteva fidarsi completamente del compilatore si è conclusa, di fatto, già dalla fine del XX secolo.

Di conseguenza, per avere almeno un significato, la condizione deve essere tale da rendere difficile, dal punto di vista del compilatore, la previsione e la sostituzione con un'altra istruzione.

```go
package main

import (
    "os"
    "strconv"
)

func main() {
    // Se si sostituisse questo con un'espressione prevedibile e in grado di eliminare il ramo, come x = 10,
    // il compilatore ottimizzerebbe il codice eliminando il ramo.
    // Pertanto, per osservare direttamente questo comportamento in assembly, in C si usa -O0 o simili,
    // oppure si fa in modo che il compilatore usi un valore esterno imprevedibile;
    // poiché questa sezione tratta la programmazione moderna, non useremo
    // il metodo di disattivare l'ottimizzazione binaria di 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")
    }
}
```

In questo caso, poiché il compilatore non può prevedere l'input, il ramo viene tradotto in linguaggio macchina così com'è.

### Linguaggio 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)
```

Lo strumento go tool, con gentilezza, indica una per una quale istruzione sorgente corrisponda a quale codice assembly.

Poiché in questa occasione studieremo le espressioni di confronto e le istruzioni di ramificazione if, è sufficiente prestare attenzione ad alcune righe.

### Istruzioni CMPQ e JL

L'istruzione `CMPQ` serve a confrontare tipi di dato di 4 byte (4 word) e deriva dall'abbreviazione di **C**o**MP**are**Q**uadword, cioè **CMPQ**.

All'indirizzo di memoria `0x47a84e` si trova l'istruzione `CMPQ os.Args+8(SB), $0x2`.
In questo caso, il programma confronta il numero di argomenti ricevuti con il valore esadecimale `0x2` (cioè semplicemente 2).

Successivamente, se il numero di argomenti è minore di 2 (cioè se il programma riceve soltanto se stesso come argomento), viene eseguito un salto condizionale tramite `JL`. Quindi questa istruzione è l'abbreviazione di **J**ump, **L**ess than, cioè **JL**.
Se, in base al confronto precedente, gli argomenti erano minori di 2, il programma salta all'indirizzo `0x47a8b0`, dove si trova un'istruzione `JGE`.
Tuttavia, in questa sequenza viene utilizzato il registro `AX`, quindi è necessario conoscere il significato del valore memorizzato nel registro.

### Istruzione MOVQ

Successivamente, occorre capire come, utilizzando il registro `CX`, venga memorizzato l'indirizzo iniziale dei dati, e come vengano poi estratti i dati reali dopo la lettura dell'indirizzo.

Osservando l'intervallo `0x47858`-`0x47863`, questa operazione viene eseguita per fasi.

Anzitutto, l'indirizzo iniziale dell'array degli argomenti viene inserito nel registro CX tramite l'istruzione `MOVQ os.Args(SB), CX`. A questo punto è necessario comprendere il tipo stringa di Go.

La `string` di Go è una struttura, composta da due dati di 8 byte, per un totale di 16 byte.

| struct | 8 byte      | 8 byte        |
| ------ | ----------- | ------------- |
| string | mem address | string length |

Visivamente, la struttura è quella mostrata sopra: i primi 8 byte contengono l'indirizzo iniziale della stringa, mentre gli ultimi 8 byte contengono la lunghezza della stringa.

Pertanto, l'indirizzo della stringa viene memorizzato nel registro AX e la sua lunghezza nel registro BX.

### CALL

Anche nel post precedente, quando si osservavano le funzioni del gruppo `runtime`, comparivano istruzioni precedute da CALL.
Questa istruzione precede le funzioni utilizzate in Go e, come suggerisce il nome, significa letteralmente **chiamare una funzione**. Successivamente, tramite la chiamata a funzione, la stringa viene convertita in un intero, ma dove venga memorizzato tale intero _non è visibile, poiché è astratto all'interno della funzione._

### Istruzioni CMPQ e JGE

Tornando all'indirizzo `0x47a86c`, l'istruzione confronta **l'indirizzo della stringa con il numero `0xa` (decimale 10)!**

Ciò significa che, poiché il programma non utilizza più quell'argomento, **il valore è stato sovrascritto nella posizione della stringa, creando così lo spazio per la variabile intera `x`.**

Questa è la realtà dell'ottimizzazione aggressiva che avviene in linguaggi come Go.

Successivamente compare l'istruzione JGE, abbreviazione di **J**ump, **G**reater or **E**quals. Quindi questa sequenza verifica se il valore è maggiore o uguale rispetto al termine di confronto.

Pertanto, anziché la forma originale `x < 10`, si ha una condizione in cui **la direzione del confronto risulta invertita rispetto a `x < 10`!**
Ciò avviene perché, a livello di linguaggio macchina, saltare in anticipo quando la condizione non è soddisfatta è più diretto e fa **risparmiare un'istruzione**, rispetto all'esecuzione del confronto nel caso in cui la condizione sia soddisfatta, seguita dalla verifica che essa non sia soddisfatta.

Questo tipo di ottimizzazione è molto classico e, a differenza dell'esempio `strconv.Atoi` visto sopra, compare frequentemente anche nei compilatori con un livello di ottimizzazione piuttosto basso; è quindi utile conoscerlo.

Pertanto, applicando questo principio, è possibile ottenere sorgenti diverse che producono assembly identici al 100%.

### Esempio di codice speculare

Utilizzando lo script seguente, lo script bash genera due sorgenti speculari al 100%, e, escludendo i metadati che di volta in volta variano, è possibile verificare che considerando solo `main` si ottiene un assembly esattamente identico.

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

# 1. Inizializzazione completa dei residui e delle directory precedenti
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. Scrittura del codice sorgente della versione originale (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. Scrittura del codice sorgente della versione speculare (main_from_asm.go)
# Mantenendo la struttura del confronto x < 10, il compilatore adotta
# esattamente lo stesso meccanismo JGE e la stessa disposizione dei blocchi di main.go
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"

        // Prendendo 10 come riferimento e mantenendo la struttura x < 10, il compilatore
        // adotta esattamente lo stesso meccanismo JGE e la stessa disposizione dei blocchi di main.go.
        switch {
        case x < 10:
                println(s1)
        default:
                println(s2)
        }
}
EOF

# 4. Compilazione di entrambi i sorgenti nello stesso percorso di directory e nome file
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. Estrazione della sezione assembly della funzione main.main tramite 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

# Rimozione degli indirizzi virtuali, degli offset e dei dati dei byte macchina,
# filtrando solo il set di istruzioni eseguite dalla CPU (Opcode e Operandi)
awk '{print $4, $5, $6, $7}' orig.asm > orig_pure.asm
awk '{print $4, $5, $6, $7}' asm.asm > asm_pure.asm

# 6. Verifica della struttura delle istruzioni macchina tramite 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 "------------------------------------------------------------"
```

Eseguendo effettivamente il codice, si ottengono le seguenti informazioni.

```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% 일치합니다! <===
컴파일러의 최적화 파이프라인 가이드라인을 완벽하게 동기화하여 동일한 어셈블리를 얻었습니다.
------------------------------------------------------------
```

### Conclusione

I linguaggi di programmazione offrono molte astrazioni, ma dietro di esse si nascondono ottimizzazioni davvero interessanti e aggressive. Inoltre, sfruttando questo aspetto in senso inverso, è stato possibile creare codice speculare: sorgenti diversi che producono lo stesso assembly. Se si è interessati al basso livello e ci si imbatte in software proprietario scritto in Go, scomporre l'assembly per analizzarlo e recuperare il sorgente non sembra un'impresa impossibile.

## Prossima lezione

Nella prossima lezione esploreremo, oltre alle istruzioni If, anche l'istruzione `select-case`, che offre un altro motivo di interesse.
