Node.js por Baixo dos Panos #8 - Entendendo Bytecodes
Tags: node, javascript, v8, assembly
Published: 2020-03-18 07:42:45

Ultimamente falamos muito de bytecodes. Mas o que são esses bytecodes?

Bytecodes são abstrações de códigos de máquina. Pense neles como algo entre o código que podemos ler e o codigo que as máquinas executam. No entanto, os bytecodes são **independentes de arquitetura**, o que significa que eles podem ser compilados em qualquer arquitetura de máquina em que você esteja executando - no entanto, compilar bytecode em código de máquina é muito mais fácil se você gerar bytecode que foi projetado com o mesmo modelo computacional da CPU que o está executando.

As CPUs são máquinas de Turing que são baseadas em [pilhas](https://docs.microsoft.com/dotnet/api/system.collections.stack?WT.mc_id=devto-blog-ludossan), registradores ou estados. O interpretador Ignition do V8 é um **interpretador baseado em registro com um acumulador**, assim como a maioria das CPUs.

![](https://github.com/khaosdoctor/my-notes/raw/master/node/assets/ignition-bytecode.png)

No final, o bytecode é traduzido em código assembly/máquina que pode ser enviado ao processador e executado.

Você pode pensar no [JavaScript](https://visualstudio.microsoft.com/vs/features/javascript/?WT.mc_id=devto-blog-ludossan) como uma série de pequenos blocos de construção. Cada operador (ou conjuntos de operadores) possui uma notação de bytecode no V8. Portanto, temos bytecodes para operadores como `typeof`, `add`, `sub` e também temos operadores para carregar propriedades como `LdaSmi` para números inteiros pequenos ou `LdaNamedProperty`. A lista completa pode ser encontrada no [arquivo de header] (https://github.com/v8/v8/blob/master/src/interpreter/bytecodes.h)

## Registradores

O Ignition usa registradores como `r0, r1, r2 ...` para armazenar entradas ou saídas de bytecodes e especifica quais estamos usando. Juntamente com os registrador de entrada, o Ignition também possui um acumulador, que armazena os resultados das operações, que chamaremos de `acc`. É praticamente o mesmo que registradores comuns, mas operandos não o especificam, por exemplo, `sub r0` está subtraindo o valor em `r0` **do** valor **no** do acumulador, deixando o resultado no próprio `acc`.

Você verá que muitos bytecodes começam com `Lda` ou `Sta`, o `a` significa "acumulador", enquanto `Ld` é "load" e `St` é "store". Assim, por intuição, `LdaSmi [99]` carrega o número inteiro `99` no acumulador, enquanto `Star r0` armazena o valor do acumulador no registro `r0`.

Isso ocorre porque se escrevemos: "LoadSmallIntToAccumulator" em vez de "LdaSmi", teríamos que alocar mais memória apenas para armazenar o nome do bytecode. É por isso que os bytecodes assustam muitas pessoas.

> Bytecodes menores = Menos memória usada

## Hands-on

Vamos pegar um bytecode real de uma função real em JavaScript. Não estamos usando nossa função `readFile`, pois seria muito complicado. Vamos usar esta função simples:

```js
function multiplyXByY (obj) {
  return obj.x * obj.y
}

multiplyXByY({ x: 1, y: 2 })
```

> *Uma pequena observação*: O compilador V8 é _lazy_, portanto, se você não executar uma função, ela não será compilada, o que significa que não gera nenhum bytecode.

Isso vai gerar o seguinte bytecode:

```
[generated bytecode for function: multiplyXByY]
Parameter count 2
Register count 1
Frame size 8
   22 E> 0x334a92de11fe @    0 : a5                StackCheck
   43 S> 0x334a92de11ff @    1 : 28 02 00 01       LdaNamedProperty a0, [0], [1]
         0x334a92de1203 @    5 : 26 fb             Star r0
   51 E> 0x334a92de1205 @    7 : 28 02 01 03       LdaNamedProperty a0, [1], [3]
   45 E> 0x334a92de1209 @   11 : 36 fb 00          Mul r0, [0]
   52 S> 0x334a92de120c @   14 : a9                Return
Constant pool (size = 2)
Handler Table (size = 0)
```

> Você pode obter os mesmos resultados executando o comendo `node --print-bytecode --print-bytecode-filter=nomeDaFuncao <arquivo>`

Vamos ignorar o cabeçalho e o rodapé porque são apenas metadados.

**LdaNamedProperty a0, [0], [1]**

Este bytecode carrega uma propriedade nomeada de `a0` - O Ignition identifica parâmetros da função como `a0, a1, a2 ... `, o número é o índice do argumento, portanto `a0` é o primeiro argumento da função (`obj`) - para o acumulador.

Neste bytecode em particular, procuramos a propriedade nomeada em `a0`, portanto, estamos carregando o primeiro argumento da função, que é `obj`. O nome que estamos vendo é determinado pelo primeiro parâmetro: `[0]`. Essa constante é usada para procurar o nome em uma tabela separada - que pode ser acessada na parte `Constant Pool` do output, mas apenas no modo de debug do Node.js.

```
0x263ab302cf21: [FixedArray] in OldSpace
 - map = 0x2ddf8367abce <Map(HOLEY_ELEMENTS)>
 - length: 2
           0: 0x2ddf8db91611 <String[1]: x>
           1: 0x2ddf8db67544 <String[1]: y>
```

Então vemos que a posição 0 é `x`. O `[1]` é o índice do que é chamado "vetor de feedback", que contém informações de tempo de execução que são usadas para otimizações.

**Star r0**

`Star r0` armazena o valor que está atualmente no acumulador, que é o valor do índice `x` que acabamos de carregar, no registro `r0`.

**LdaNamedProperty a0, [1], [3]**

É a mesma coisa, mas agora estamos carregando o índice 1, que é `y`.

**Mul r0, [0]**

Esta operação multiplica o valor que está atualmente no acumulador (`y`) por `r0` (`x`) e armazena o resultado no acumulador.

**Return**

A declaração de retorno retorna o valor que está atualmente no acumulador. É também o fim da função. Portanto, o chamador da função começará com o resultado da nossa última operação de bytecode - que é `2` - já no acumulador.

## O que devemos saber

A maioria dos bytecodes pode parecer sem sentido à primeira vista. Mas lembre-se de que o Ignition é uma máquina de registro com um acumulador, é basicamente assim que podemos facilmente entender como funciona.

Este seria o bytecode para a nossa função `readFile`:

```
[generated bytecode for function: readFileAsync]
Parameter count 2
Register count 3
Frame size 24
         0x23e95d8a1ef6 @    0 : 84 00 01          CreateFunctionContext [0], [1]
         0x23e95d8a1ef9 @    3 : 16 fb             PushContext r0
         0x23e95d8a1efb @    5 : 25 02             Ldar a0
         0x23e95d8a1efd @    7 : 1d 04             StaCurrentContextSlot [4]
  261 E> 0x23e95d8a1eff @    9 : a5                StackCheck
  279 S> 0x23e95d8a1f00 @   10 : 13 01 00          LdaGlobal [1], [0]
         0x23e95d8a1f03 @   13 : 26 fa             Star r1
         0x23e95d8a1f05 @   15 : 81 02 00 02       CreateClosure [2], [0], #2
         0x23e95d8a1f09 @   19 : 26 f9             Star r2
         0x23e95d8a1f0b @   21 : 25 fa             Ldar r1
  286 E> 0x23e95d8a1f0d @   23 : 65 fa f9 01 02    Construct r1, r2-r2, [2]
  446 S> 0x23e95d8a1f12 @   28 : a9                Return
Constant pool (size = 3)
Handler Table (size = 0)
```

Podemos ver que ele possui uma série de bytecodes projetados especificamente para vários aspectos da linguagem, como fechamentos, globais e assim por diante... Você consegue ler esse bytecode? Deixe aqui nos comentários :)

Não deixe de acompanhar mais do meu conteúdo no [meu blog](https://blog.lsantos.dev/?utm_source=devto&utm_medium=article&utm_campaign=node_under_the_hood) e se [inscreva na newsletter](https://blog.lsantos.dev/signup?utm_source=devto&utm_medium=article&utm_campaign=node_under_the_hood) para receber notícias semanais!

#### Agradecimentos

Um grande obrigado a [Franziska Hinkelmann](https://twitter.com/fhinkel), seus artigos e palestras sobre os bytecodes da V8 são simplesmente incríveis e me ajudaram muito quando comecei a estudar esse tópico. Especialmente [este](https://medium.com/dailyjs/understanding-v8s-bytecode-317d46c94775)!