DEV Community

Cover image for Node.js por Baixo dos Panos #8 - Entendendo Bytecodes
Lucas Santos
Lucas Santos

Posted on • Updated on

Node.js por Baixo dos Panos #8 - Entendendo Bytecodes

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, registradores ou estados. O interpretador Ignition do V8 é um interpretador baseado em registro com um acumulador, assim como a maioria das CPUs.

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

Você pode pensar no JavaScript 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

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:

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

multiplyXByY({ x: 1, y: 2 })
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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 e se inscreva na newsletter para receber notícias semanais!

Agradecimentos

Um grande obrigado a Franziska Hinkelmann, 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!

Top comments (0)