Análise Aprofundada do Código Fonte do Chrome V8: Desvendando os 'Builtins'
Introdução
As próximas seções deste artigo se dedicarão a uma análise aprofundada dos Built-in Functions (Builtins) do V8. Os Builtins implementam uma vasta gama de funcionalidades centrais no V8, o que evidencia sua importância crucial. No entanto, a maioria dos Builtins é implementada utilizando CAS (CodeStubAssembler) e Torque, linguagens que se assemelham à linguagem de montagem (assembly), apresentando desafios significativos para a leitura do código fonte. Agravando a dificuldade, não é possível depurar Builtins durante a execução do V8, tornando o aprendizado ainda mais árduo. Portanto, este artigo visa apresentar métodos detalhados para o estudo e depuração de Builtins, na esperança de estimular discussões e aprendizado.
1. Resumo
Este é o primeiro artigo de uma série focada em Builtins, abordando o que são os Built-in Functions (Builtins) e como ocorre sua inicialização. Os Built-in Functions, como funcionalidades nativas do V8, são responsáveis pela implementação de muitas operações importantes, incluindo o motor Ignition, manipuladores de bytecode e APIs JavaScript. Dominar os Builtins é fundamental para compreender a lógica de execução do V8, permitindo, por exemplo, visualizar como o bytecode é executado ou como métodos de string como substring são implementados. O conteúdo principal deste artigo aborda os métodos de implementação de Builtins (Seção 2) e a inicialização de Builtins (Seção 3).
2. Métodos de Implementação de Builtins
Os Builtins podem ser implementados de cinco maneiras distintas: linguagem de montagem dependente da plataforma (Platform-dependent assembly language), C++, JavaScript, CodeStubAssembler (CAS) e Torque. Essas abordagens variam significativamente em termos de facilidade de uso e performance. Conforme a documentação oficial (v8.dev/docs/torque):
- Linguagem de Montagem Dependente da Plataforma: Pode ser altamente eficiente, mas requer portabilidade manual para todas as plataformas e é difícil de manter.
- C++: Similar em estilo às funções de runtime e com acesso às poderosas funcionalidades de runtime do V8, mas geralmente não é adequada para áreas sensíveis à performance.
- JavaScript: Código conciso e legível, com acesso a intrínsecos rápidos, mas com chamadas frequentes e lentas ao runtime, sujeito a performance imprevisível devido à poluição de tipos e questões sutis relacionadas à semântica complexa e não óbvia do JavaScript. Builtins em JavaScript estão obsoletos e não devem mais ser adicionados.
- CodeStubAssembler (CAS): Oferece funcionalidades eficientes de baixo nível, muito próximas à linguagem de montagem, mas mantendo a independência de plataforma e a legibilidade.
- V8 Torque: Uma linguagem de domínio específico do V8 que é traduzida para CodeStubAssembler. Como tal, estende o CAS, oferecendo tipagem estática e uma sintaxe legível e expressiva.
Torque é uma versão aprimorada do CodeStubAssembler, focada em reduzir a complexidade de uso sem sacrificar a performance, tornando o desenvolvimento de Builtins mais acessível. O diagrama oficial (Figura 1) ilustra o processo de criação de Builtins utilizando Torque. Inicialmente, o arquivo file.tq escrito pelo desenvolvedor é traduzido pelo compilador Torque para arquivos -tq-csa.cc/.h. Em seguida, esses arquivos são compilados no executável mksnapshot. Finalmente, mksnapshot gera o arquivo snapshot.bin, que armazena a representação binária serializada dos Builtins. É importante ressaltar que os arquivos -tq-csa.cc/.h são o código fonte dos Builtins gerado pelo compilador Torque a partir do arquivo file.tq. Ao carregar o arquivo snapshot.bin através de desserialização, o V8 não possui tabelas de símbolos, o que impede a visualização do código fonte dos Builtins Torque durante a depuração do código fonte do V8. Os Builtins do CodeStubAssembler também são armazenados em snapshot.bin, tornando-os invisíveis durante a depuração. Métodos de depuração serão abordados posteriormente.
3. Inicialização de Builtins
Antes de mergulharmos no código fonte, é crucial notar algumas considerações. Para depurar a inicialização de Builtins, utilizaremos a versão 7.9 do V8 com a opção v8_use_snapshot ativada. Versões mais recentes não suportam mais v8_use_snapshot = false, o que nos impede de depurar a inicialização de Builtins. A configuração v8_use_snapshot = false desabilita o arquivo snapshot.bin, forçando o V8 a criar e inicializar os Builtins a partir do código fonte C++ durante a inicialização, o que é exatamente o que desejamos observar. Consideramos que os Builtins implementados em C++, CodeStubAssembler e Torque são os mais importantes, pois funcionalidades centrais como o Ignition, manipuladores de bytecode e APIs JavaScript são majoritariamente implementadas por eles. Abaixo, detalhamos essas três categorias de Builtins. O ponto de entrada para a inicialização de Builtins é o seguinte código:
cppbool Isolate::InitWithoutSnapshot() { return Init(nullptr, nullptr); }
Como o nome sugere, InitWithoutSnapshot() desabilita o uso do arquivo snapshot.bin. A função InitWithoutSnapshot() executa o seguinte código:
cppbool Isolate::Init(ReadOnlyDeserializer* read_only_deserializer, StartupDeserializer* startup_deserializer) { // ... (código omitido) ... bootstrapper_->Initialize(create_heap_objects); if (FLAG_embedded_builtins && create_heap_objects) { builtins_constants_table_builder_ = new BuiltinsConstantsTableBuilder(this); } setup_delegate_->SetupBuiltins(this); if (FLAG_embedded_builtins && create_heap_objects) { builtins_constants_table_builder_->Finalize(); delete builtins_constants_table_builder_; builtins_constants_table_builder_ = nullptr; CreateAndSetEmbeddedBlob(); } // ... (código omitido) ... return true; }
A linha 8, setup_delegate_->SetupBuiltins(this);, direciona a execução para SetupBuiltins(), que por sua vez chama SetupBuiltinsInternal() para completar a inicialização dos Builtins. O código fonte de SetupBuiltinsInternal() é o seguinte:
cppvoid SetupIsolateDelegate::SetupBuiltinsInternal(Isolate* isolate) { Builtins* builtins = isolate->builtins(); // ... (código omitido) ... int index = 0; Code code; #define BUILD_CPP(Name) code = BuildAdaptor(isolate, index, FUNCTION_ADDR(Builtin_##Name), #Name); AddBuiltin(builtins, index++, code); #define BUILD_TFJ(Name, Argc, ...) code = BuildWithCodeStubAssemblerJS( isolate, index, &Builtins::Generate_##Name, Argc, #Name); AddBuiltin(builtins, index++, code); #define BUILD_TFC(Name, InterfaceDescriptor) /* Return size is from the provided CallInterfaceDescriptor. */ code = BuildWithCodeStubAssemblerCS( isolate, index, &Builtins::Generate_##Name, CallDescriptors::InterfaceDescriptor, #Name); AddBuiltin(builtins, index++, code); #define BUILD_TFS(Name, ...) /* Return size for generic TF builtins (stub linkage) is always 1. */ code = BuildWithCodeStubAssemblerCS(isolate, index, &Builtins::Generate_##Name, CallDescriptors::Name, #Name); AddBuiltin(builtins, index++, code); #define BUILD_TFH(Name, InterfaceDescriptor) /* Return size for IC builtins/handlers is always 1. */ code = BuildWithCodeStubAssemblerCS( isolate, index, &Builtins::Generate_##Name, CallDescriptors::InterfaceDescriptor, #Name); AddBuiltin(builtins, index++, code); #define BUILD_BCH(Name, OperandScale, Bytecode) code = GenerateBytecodeHandler(isolate, index, OperandScale, Bytecode); AddBuiltin(builtins, index++, code); #define BUILD_ASM(Name, InterfaceDescriptor) code = BuildWithMacroAssembler(isolate, index, Builtins::Generate_##Name, #Name); AddBuiltin(builtins, index++, code); BUILTIN_LIST(BUILD_CPP, BUILD_TFJ, BUILD_TFC, BUILD_TFS, BUILD_TFH, BUILD_BCH, BUILD_ASM); // ... (código omitido) ... }
As três funcionalidades centrais de SetupBuiltinsInternal() são explicadas a seguir:
-
Diferenciação de Builtins: Os macros
BUILD_CPP,BUILD_TFJ,BUILD_TFC,BUILD_TFS,BUILD_TFH,BUILD_BCHeBUILD_ASMdiferenciam os Builtins por sua funcionalidade, conforme os comentários:CPP: Builtin em C++. Acessado via frameBUILTIN_EXIT. Argumentos: nome.TFJ: Builtin em Turbofan, com linkage JS (chamável como função JavaScript). Argumentos: nome, número de argumentos, nomes explícitos de argumentos...TFS: Builtin em Turbofan, com linkage CodeStub. Argumentos: nome, nomes explícitos de argumentos...TFC: Builtin em Turbofan, com linkage CodeStub e descritor customizado. Argumentos: nome, descritor de interface.TFH: Handlers em Turbofan, com linkage CodeStub. Argumentos: nome, descritor de interface.BCH: Bytecode Handlers, com linkage de dispatch de bytecode. Argumentos: nome, escala de operando, bytecode.ASM: Builtin em montagem dependente da plataforma. Argumentos: nome, descritor de interface.
-
Definição de Builtins: A linha 38 de
SetupBuiltinsInternal(),BUILTIN_LIST, define todos os Builtins. O código fonte é:cpp#define BUILTIN_LIST(CPP, TFJ, TFC, TFS, TFH, BCH, ASM) \ BUILTIN_LIST_BASE(CPP, TFJ, TFC, TFS, TFH, ASM) \ BUILTIN_LIST_FROM_TORQUE(CPP, TFJ, TFC, TFS, TFH, ASM) \ BUILTIN_LIST_INTL(CPP, TFJ, TFS) \ BUILTIN_LIST_BYTECODE_HANDLERS(BCH) //================分隔线================================= #define BUILTIN_LIST_FROM_TORQUE(CPP, TFJ, TFC, TFS, TFH, ASM) //............... (código omitido) ............................ TFJ(StringPrototypeToString, 0, kReceiver) TFJ(StringPrototypeValueOf, 0, kReceiver) TFS(StringToList, kString) TFJ(StringPrototypeCharAt, 1, kReceiver, kPosition) TFJ(StringPrototypeCharCodeAt, 1, kReceiver, kPosition) TFJ(StringPrototypeCodePointAt, 1, kReceiver, kPosition) TFJ(StringPrototypeConcat, SharedFunctionInfo::kDontAdaptArgumentsSentinel) TFJ(StringConstructor, SharedFunctionInfo::kDontAdaptArgumentsSentinel) TFS(StringAddConvertLeft, kLeft, kRight) TFS(StringAddConvertRight, kLeft, kRight) TFJ(StringPrototypeEndsWith, SharedFunctionInfo::kDontAdaptArgumentsSentinel) TFS(CreateHTML, kReceiver, kMethodName, kTagName, kAttr, kAttrValue) TFJ(StringPrototypeAnchor, SharedFunctionInfo::kDontAdaptArgumentsSentinel) TFJ(StringPrototypeBig, SharedFunctionInfo::kDontAdaptArgumentsSentinel) TFJ(StringPrototypeIterator, 0, kReceiver) TFJ(StringIteratorPrototypeNext, 0, kReceiver) TFJ(StringPrototypePadStart, SharedFunctionInfo::kDontAdaptArgumentsSentinel) TFJ(StringPrototypePadEnd, SharedFunctionInfo::kDontAdaptArgumentsSentinel) TFS(StringRepeat, kString, kCount) TFJ(StringPrototypeRepeat, 1, kReceiver, kCount) TFJ(StringPrototypeSlice, SharedFunctionInfo::kDontAdaptArgumentsSentinel) TFJ(StringPrototypeStartsWith, SharedFunctionInfo::kDontAdaptArgumentsSentinel) TFJ(StringPrototypeSubstring, SharedFunctionInfo::kDontAdaptArgumentsSentinel)
A combinação de BUILTIN_LIST e BUILTIN_LIST_FROM_TORQUE revela os nomes de todos os Builtins. As linhas 9-31 mostram os nomes dos Builtins que implementam métodos de string, como StringPrototypeSubstring para a função substring.
-
Inicialização de Builtins: Os sete macros (
BUILD_CPP,BUILD_TFJ, etc.) em conjunto comBUILTIN_LISTcompletam a inicialização de todos os Builtins. Analisando o macroBUILD_CPPemSetupBuiltinsInternal():cppint index = 0; Code code;
#define BUILD_CPP(Name) code = BuildAdaptor(isolate, index, FUNCTION_ADDR(Builtin_##Name), #Name); AddBuiltin(builtins, index++, code); //................... (linha separadora) ................. // FUNCTION_ADDR(f) gets the address of a C function f. #define FUNCTION_ADDR(f) (reinterpret_castv8::internal::Address(f)) ```
`index` é inicializado em 0, e `code` é um ponteiro de endereço baseado em `HeapObject`, usado para armazenar o endereço do Builtin gerado. `FUNCTION_ADDR(Builtin_##Name)` cria o ponteiro de endereço do Builtin, que será utilizado na criação do Builtin dentro de `BuildAdaptor()`. O código fonte de `BuildAdaptor()` é:
```cpp
Code BuildAdaptor(Isolate* isolate, int32_t builtin_index,
Address builtin_address, const char* name) {
HandleScope scope(isolate);
// Canonicalize handles, so that we can share constant pool entries pointing
// to code targets without dereferencing their handles.
CanonicalHandleScope canonical(isolate);
constexpr int kBufferSize = 32 * KB;
byte buffer[kBufferSize];
MacroAssembler masm(isolate, BuiltinAssemblerOptions(isolate, builtin_index),
CodeObjectRequired::kYes,
ExternalAssemblerBuffer(buffer, kBufferSize));
masm.set_builtin_index(builtin_index);
DCHECK(!masm.has_frame());
Builtins::Generate_Adaptor(&masm, builtin_address);
CodeDesc desc;
masm.GetCode(isolate, &desc);
Handle<Code> code = Factory::CodeBuilder(isolate, desc, Code::BUILTIN)
.set_self_reference(masm.CodeObject())
.set_builtin_index(builtin_index)
.Build();
return *code;
}
```
Neste código, a criação do Builtin é realizada através de `Generate_Adaptor` e `Factory::CodeBuilder`, onde `code` representa o endereço do Builtin. Retornando a `#define BUILD_CPP(Name)`, a execução prossegue para `AddBuiltin()`:
```cpp
void SetupIsolateDelegate::AddBuiltin(Builtins* builtins, int index,
Code code) {
DCHECK_EQ(index, code.builtin_index());
builtins->set_builtin(index, code);
}
//.............. (linha separadora) ........................
void Builtins::set_builtin(int index, Code builtin) {
isolate_->heap()->set_builtin(index, builtin);
}
//............. (linha separadora) ...........................
void Heap::set_builtin(int index, Code builtin) {
DCHECK(Builtins::IsBuiltinId(index));
DCHECK(Internals::HasHeapObjectTag(builtin.ptr()));
// The given builtin may be completely uninitialized thus we cannot check its
// type here.
isolate()->builtins_table()[index] = builtin.ptr();
}
```
Neste trecho, `Builtins::set_builtin()` chama `Heap::set_builtin()` para armazenar o Builtin na tabela `isolate()->builtins_table()`. `builtins_table` é um array do tipo `V8_INLINE Address*`, onde `index` é o índice do array. Este array armazena todos os Builtins. Com isso, a inicialização dos Builtins é concluída. A Figura 2 ilustra a pilha de chamadas de função.
Resumo dos Métodos de Depuração de Builtins:
- Expansão de Macros: Expanda o macro
BUILTIN_LISTpara obter o número de índice (index) de cada Builtin. Ferramentas como o pré-processador do VS2019 podem auxiliar nessa expansão. - Pontos de Interrupção Condicionais: Utilize o
indexpara definir pontos de interrupção condicionais. A Figura 3 demonstra como rastrear o Builtin de número 12. Definir um ponto de interrupção diretamente no código fonte do Builtin é o método mais direto. Se você não tem certeza de como um Builtin é implementado (por exemplo,BUILD_CPPouBUILD_TFS), defina pontos de interrupção condicionais em cada método. A Figura 4 mostra um ponto de interrupção definido no código fonte deSubstring.
Resumo Técnico:
- Ao depurar Builtins, utilize a versão 7.x do V8, pois as versões mais recentes não possuem mais a opção
v8_use_snapshot. - Ao compilar o V8, defina
v8_optimized_debug = falsepara desativar as otimizações do compilador. - Como
builtin_indexé do tipoint32_t, ao definir pontos de interrupção condicionais, utilize(int)builtin_index.
Por hoje é só. Até a próxima!





