GoSuda

Instrução If na linguagem Go

By Lee Yunjin
views ...

Instruções If na linguagem Go

Primeiramente, optamos pela linguagem Go devido ao fato de que, entre as linguagens modernas, ela possui o assembly mais "elegante" e, mesmo quando comparada a linguagens clássicas, sua eficiência sintática é frequentemente avassaladora.

Agora que compreendemos o funcionamento básico de um programa em Go na aula anterior, passemos imediatamente a comparar, linha por linha, o código em Go com o seu respectivo Assembly.

Código-fonte

De início, embora seja uma prática comum em Go, até mesmo compiladores modernos, incluindo o GCC, otimizam automaticamente instruções de desvio (branching) que não possuem utilidade prática. Compiladores de C, como o GCC e o Clang, realizam otimizações extremamente agressivas no padrão da indústria -O2; portanto, a era em que o programador não podia confiar plenamente no compilador foi, de fato, consolidada desde o final do século XX.

Consequentemente, a instrução só adquire significado se fornecermos condições que sejam difíceis para o compilador prever e converter em outras estruturas.

 1package main
 2
 3import (
 4    "os"
 5    "strconv"
 6)
 7
 8func main() {
 9    // Se preenchermos isto com algo previsível como x = 10, o que permitiria 
10    // a remoção do desvio, o compilador irá otimizar e eliminar a ramificação.
11    // Portanto, para observar isso diretamente em assembly, em C utilizaríamos 
12    // -O0 ou faríamos com que o compilador utilizasse valores externos imprevisíveis.
13    // Como esta seção aborda a programação moderna, não utilizaremos 
14    // métodos para desativar a otimização de binários em Go.
15    if len(os.Args) < 2 {
16        return
17    }
18    x, _ := strconv.Atoi(os.Args[1])
19
20    if x < 10 {
21        println("X is smaller than 10")
22    } else {
23        println("X is larger or same as 10")
24    }
25}

Neste caso, como o compilador não consegue prever a entrada, a instrução de desvio é traduzida literalmente para linguagem de máquina.

Linguagem Assembly

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

Ao utilizar a go tool, somos gentilmente informados sobre a correspondência entre cada instrução e o código assembly gerado.

Como o foco desta lição são as instruções de comparação e de desvio if, devemos atentar para algumas linhas específicas.

Instrução CMPQ & Instrução JL

A instrução CMPQ é utilizada para comparar tipos de dados de 4 bytes (4 words), sendo derivada de CoMPare Quadword.

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

Em seguida, através da instrução JL, o programa realiza um desvio caso o número de argumentos seja menor que 2 (ou seja, se o programa for o único argumento). Isto deriva de Jump, Less than. Se o número de argumentos for menor que 2 na comparação anterior, ele salta para o endereço 0x47a8b0, onde encontramos a instrução JGE. Entretanto, como esta instrução utiliza o registrador AX, precisamos compreender a natureza do valor armazenado nele.

Instrução MOVQ

A seguir, devemos entender como o endereço inicial dos dados é armazenado utilizando o registrador CX e como os dados reais são extraídos após a leitura do endereço.

Ao analisar o intervalo 0x47858-0x47863, observamos a execução gradual desta operação.

Primeiro, o endereço inicial do array de argumentos é inserido no registrador CX por meio da instrução MOVQ os.Args(SB), CX. Neste ponto, é necessário compreender o tipo string em Go.

O tipo string em Go é uma estrutura (struct) composta por 16 bytes, divididos em dois campos de 8 bytes cada.

struct8 byte8 byte
stringmem addressstring length

Representando visualmente, os primeiros 8 bytes contêm o endereço inicial da string, e os 8 bytes subsequentes contêm o comprimento da string.

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

CALL

Nas postagens anteriores, ao observarmos as funções da runtime, a instrução CALL já estava presente. Esta instrução precede as funções utilizadas em Go e significa, literalmente, chamar uma determinada função. Posteriormente, a função CALL é utilizada para converter a string em um número inteiro, embora o local de armazenamento desse inteiro não seja visível, pois está abstraído pela função.

Instrução CMPQ & Instrução JGE

Retornando ao endereço 0x47a86c, a instrução compara o endereço da string com o valor numérico 0xa (10 em decimal)!

Isso significa que, como o argumento não é mais utilizado dentro do programa, o compilador sobrescreveu o local da string para criar o espaço da variável inteira x.

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

Em seguida, surge a instrução JGE, que é a abreviação de Jump, Greater or Equals. Portanto, esta instrução verifica se o valor é maior ou igual ao termo de comparação.

Assim, não se trata da instrução x < 10 literalmente, mas sim de uma inversão na direção da comparação! Isso ocorre porque, em linguagem de máquina, pular preventivamente quando a condição não é atendida é mais intuitivo e economiza uma instrução do que realizar a comparação quando a condição é atendida e verificar novamente se ela não é verdadeira.

Essa otimização é clássica e, ao contrário do exemplo de strconv.Atoi visto acima, é um padrão frequente até em compiladores com níveis de otimização bastante baixos, sendo, portanto, importante conhecer.

Dessa forma, aplicando esses princípios, é possível obter códigos que, embora diferentes na origem, são 100% idênticos em nível de assembly.

Exemplo de código espelho

Ao utilizar o script abaixo, é possível verificar que o script Bash gera dois códigos-fonte que são "espelhos" um do outro e, ao analisar apenas o main (excluindo metadados variáveis), obtém-se um assembly exatamente idêntico.

 1#!/usr/bin/env bash
 2
 3# 1. Inicialização completa de arquivos residuais e diretórios
 4echo "[1/6] Cleaning up old artifacts..."
 5rm -rf test_dir main_orig main_asm orig.asm asm.asm orig_pure.asm asm_pure.asm
 6mkdir -p test_dir
 7
 8# 2. Criação da versão original do código-fonte (main.go)
 9echo "[2/6] Generating main.go..."
10cat << 'EOF' > main.go
11package main
12
13import (
14        "os"
15        "strconv"
16)
17
18func main() {
19        if len(os.Args) < 2 {
20                return
21        }
22        x, _ := strconv.Atoi(os.Args[1])
23
24        s1 := "X is smaller than 10"
25        s2 := "X is larger or same as 10"
26
27        if x < 10 {
28                println(s1)
29        } else {
30                println(s2)
31        }
32}
33EOF
34
35# 3. Criação da versão espelho do código-fonte (main_from_asm.go)
36# Sincronização simétrica da estrutura de operadores para que o compilador use o template de otimização (JGE)
37echo "[3/6] Generating main_from_asm.go..."
38cat << 'EOF' > main_from_asm.go
39package main
40
41import (
42        "os"
43        "strconv"
44)
45
46func main() {
47        if len(os.Args) < 2 {
48                return
49        }
50        x, _ := strconv.Atoi(os.Args[1])
51
52        s1 := "X is smaller than 10"
53        s2 := "X is larger or same as 10"
54
55        // Ao tomar 10 como base e manter a estrutura x < 10, o compilador
56        // adota exatamente o mesmo mecanismo JGE e disposição de blocos que em main.go.
57        if x < 10 {
58                println(s1)
59        } else {
60                println(s2)
61        }
62}
63EOF
64
65# 4. Compilação de cada um mantendo o mesmo caminho de diretório e nome de arquivo
66echo "[4/6] Compiling both sources inside 'test_dir'..."
67cp main.go test_dir/main.go
68cd test_dir && go build -o ../main_orig main.go && cd ..
69
70rm test_dir/main.go
71cp main_from_asm.go test_dir/main.go
72cd test_dir && go build -o ../main_asm main.go && cd ..
73
74# 5. Extração da função assembly puramente main.main usando go tool objdump
75echo "[5/6] Extracting main.main assembly sections..."
76go tool objdump -s "main\.main" main_orig > orig.asm
77go tool objdump -s "main\.main" main_asm > asm.asm
78
79# Remoção de endereços virtuais, offsets e dados binários de máquina para
80# filtrar apenas o conjunto de instruções puras (Opcode & Operands) que a CPU executará
81awk '{print $4, $5, $6, $7}' orig.asm > orig_pure.asm
82awk '{print $4, $5, $6, $7}' asm.asm > asm_pure.asm
83
84# 6. Verificação de diff da estrutura de instruções de máquina
85echo "[6/6] Verifying assembly structural integrity via diff..."
86echo "------------------------------------------------------------"
87
88if diff orig_pure.asm asm_pure.asm > /dev/null; then
89    echo "===> [Sucesso] A lógica em linguagem de máquina de main.main nos dois binários é 100% idêntica! <==="
90    echo "Sincronizamos perfeitamente as diretrizes do pipeline de otimização do compilador para obter o mesmo assembly."
91else
92    echo "===> [Falha] Diferenças foram encontradas na estrutura das instruções de assembly. <==="
93    diff -u orig_pure.asm asm_pure.asm
94fi
95echo "------------------------------------------------------------"

Ao executar o código, obtêm-se as seguintes informações:

 1[1/6] Cleaning up old artifacts...
 2[2/6] Generating main.go...
 3[3/6] Generating main_from_asm.go...
 4[4/6] Compiling both sources inside 'test_dir'...
 5[5/6] Extracting main.main assembly sections...
 6[6/6] Verifying assembly structural integrity via diff...
 7------------------------------------------------------------
 8===> [Sucesso] A lógica em linguagem de máquina de main.main nos dois binários é 100% idêntica! <===
 9Sincronizamos perfeitamente as diretrizes do pipeline de otimização do compilador para obter o mesmo assembly.
10------------------------------------------------------------

Conclusão

Pudemos observar que, embora as linguagens de programação ofereçam diversas abstrações, por trás delas escondem-se otimizações fascinantes e agressivas. Além disso, aproveitando esses pontos, foi possível criar códigos que funcionam como espelhos: fontes diferentes, porém com assembly idêntico. Se você tiver interesse em baixo nível e se deparar com softwares proprietários escritos em Go, recuperar o código-fonte através da desmontagem e análise direta do assembly não é uma tarefa impossível.

Próxima aula

Na próxima aula, exploraremos a instrução select-case, que possui um interesse particular, assim como a instrução If.