Phase 13: Tools & Protocols

Anatomia do Programa: Um Leitor ELF do zero

Um programa compilado é um conjunto de tabelas que apontam um para o outro. Um leitor que você escreveu fornece saída que você pode verificar byte por byte.

Type: Build

Languages: Python (stdlib), C (fixture only)

Prerequisites: Phase 0 · 01 (Dev Environment) with clang installed, Python 3.10 or later, hexadecimal arithmetic

Time: ~120 minutes

Objetivos de aprendizagem

  • Estabelecer a regra de autorização para cada alvo nesta pista e os quatro limites de 17 U.S.C. 1201 ((f).
  • Leia o cabeçalho ELF, a tabela de cabeçalho secção e ambas as tabelas de cadeia com struct.unpack- Não .
  • Decodificar.symtab- e.relaregistos com os nomes do gABI e do x86-64 psABI.
  • Retorna os bytes exatos de uma função nomeada e mapeie cada arquivo compensado para a estrutura que a possui.
  • Leia cabeçalhos de programa e mapear um endereço virtual para um arquivo compensado através de PT_LOAD- Não .
  • Leia os comandos de carga de um objeto Mach-O e nomeie a ideia ELF que cada um corresponde.

Antes de começar: Autorização e Lei

Cada alvo das lições 32 a 36 é um programa que escreveu e compilado.code/fixtures/ledger.cNão há licença para ler, não há proteção para passar e não há terceiros cujo código você abre. A faixa nunca abrange proteções de software, verificações de licenças ou programas que você não possui.

A regra existe porque a lei trata o desmonte como copiar. Em Sega Enterprises Ltd. V. Accolade, Inc., 977 F.2d 1510 (9o Cir. 1992), o tribunal considerou que as cópias intermediárias feitas durante a desmontagem são cópias ao abrigo da Lei de Direitos Autorais. Ele declarou então que "quando o desmonte é a única maneira de obter acesso às ideias e elementos funcionais incorporados em um programa de computador protegido por direitos autorais e quando há uma razão legítima para procurar esse acesso, o desmonte é um uso justo da obra protegida por direitos autorais, como questão de lei".

O segundo caso é Sony Computer Entertainment, Inc. v. Connectix Corp. , 203 F.3d 596 (9th Cir. 2000). A Connectix copiou o BIOS da PlayStation enquanto construía um emulador, e o tribunal considerou essas cópias de uso justo. Ambos os casos protegiam o estudo. Nenhum protegeu o código copiado no produto final.

Dois estatutos estabelecem os limites. 17 U.S.C. 1201 ((f) permite que uma pessoa que " tenha legalmente obtido o direito de utilizar uma cópia de um programa de computador " contorne um controlo de acesso " com o único propósito de identificar e analisar os elementos do programa necessários para alcançar a interoperabilidade de um programa de computador criado de forma independente com outros programas ". Quatro limites: uma cópia legal, o propósito da interoperabilidade, elementos que não estavam já "prontamente disponíveis" e análise que não infrinja. A secção 1201 ((f) é apenas uma exceção à regra de evasão. A questão da cópia permanece com uso justo.

Na União Europeia, a Diretiva 2009/24/CE, Artigo 5 ((3)), abrange o estudo. Uma pessoa com direito de usar uma cópia pode "observar, estudar ou testar o funcionamento do programa para determinar as ideias e princípios que subjacentes a qualquer elemento do programa".

O artigo 6.o permite a decomposição sem autorização do titular dos direitos quando "é indispensável obter as informações necessárias para alcançar a interoperabilidade de um programa de computador criado de forma independente". As condições correspondem a 1201 ((f). O titular da licença realiza os atos, as informações não estavam já prontamente disponíveis e os atos permanecem dentro das partes necessárias. O artigo 6.o, n.o 2, proíbe o uso do resultado para qualquer outro objetivo ou para um programa que seja substancialmente semelhante em expressão. O artigo 8.o torna nulos os termos contratuais contrários ao artigo 6.o ou ao artigo 5.o (3)

SourceWhat it allowsConditions
17 U.S.C. 1201(f)Circumventing an access control to analyze a programLawful copy, interoperability purpose, information not readily available, no infringement
Directive 2009/24/EC Art. 5(3)Observing, studying, and testing a running programA right to use the copy, acts within normal loading and running
Directive 2009/24/EC Art. 6DecompilationLicensee, interoperability purpose, information not readily available, confined to the needed parts
Sega v. AccoladeDisassembly as fair useThe only way to reach the ideas and functional elements, a legitimate reason
Sony v. ConnectixIntermediate copies during reverse engineeringA non-infringing final product

As notas são baseadas nos estatutos e nos pareceres, e não constituem aconselhamento jurídico. Se um futuro objectivo não for seu, obtenha autorização por escrito antes de abrir o processo e mantém a autorização com as suas provas.

O problema

Compila-te .ledger.cE correu .llvm-objdump -tAs listas de saída percent_changepor valor 0000000000000000com tamanho 0000000000000010Que 16 bytes do arquivo de 1.768 bytes são essa função?R_X86_64_PC32 .rodata+0x10em compensação0x36, e os quatro bytes naquele ponto no arquivo são zero.0x10Quando a mesa começa em0x14- Não .

rea responde às perguntas de layout com inspect_binary_layout, e essa ferramenta só existe no Linux x64 com pwntools 4.15.0 atrás REA_PWNTOOLS_PYTHONNeste Mac, não está.address_to_file_offsetprecisa de uma sessão Ghidra e uma cópia completa dos bytes do arquivo original com um SHA-256 correspondente. inspect_artifactO conteúdo de um contêiner é o que ele possui por caminho e hash de conteúdo.struct.unpackOs testes comparam cada tabela comllvm-objdumptexto capturado uma vez nas acessórios.

O arquivo são tabelas que apontam para tabelas

O Sistema V gABI, capítulo 4, figura 4-1, desenha duas visões de um arquivo. A visão de ligação tem um cabeçalho ELF, uma tabela de cabeçalho de programa opcional, seções e uma tabela de cabeçalho de seção necessária. A visão de execução tem um cabeçalho ELF, uma tabela de cabeçalho de programa necessária, segmentos e uma tabela de cabeçalho de seção opcional. O gABI adiciona uma frase que rege todo o leitor: "Somente o cabeçalho ELF tem uma posição fixa no arquivo". Todas as outras estruturas são encontradas por um offset armazenado em outra estrutura.

O gABI também fixa os tipos escalares e diz que "ELF não usa campos de bits". Elf64_AddrE ...Elf64_Offsão 8 bytes, Elf64_Halfé 2, Elf64_Wordé 4, e Elf64_XwordO x86-64 psABI, secção 4. 1.1, requer ELFCLASS64- Não .ELFDATA2LSB- Não .e_machine EM_X86_64(62), e e_flags0. - pequena endea com largura fixa média struct.unpackcom um <O prefixo lê todos os registos numa chamada.

Os primeiros 64 bytes de ledger-x86_64-O1.o- Não .

text00000000: 7f45 4c46 0201 0100 0000 0000 0000 0000  .ELF............
00000010: 0100 3e00 0100 0000 0000 0000 0000 0000  ..>.............
00000020: 0000 0000 0000 0000 2804 0000 0000 0000  ........(.......
00000030: 0000 0000 4000 0000 0000 4000 0b00 0100  ....@.....@.....

Leia-os contra a página "ELF Header" da GABI. e_identé o 16 bytes 7f 45 4c 46(a magia), classe 2 (64 bits), dados 1 (little-endian), versão 1, OS ABI 0 (Sistema V), e empilhamento.

Então ...e_type 0x0001É o que éET_REL, um objeto realojável.e_machine 0x003eé 62, EM_X86_64- Não .e_entryé zero porque um objeto não tem ponto de entrada. e_phoffé zero porque um objeto não tem cabeçalhos de programa. e_shoffÉ o que é0x428, o arquivo de deslocamento da tabela de cabeçalhos de secção. A última linha mantém e_ehsize 0x40- Não .e_phentsize0 , e_phnum0 , e_shentsize 0x40- Não .e_shnum 0x0b(11 cabeçalhos), e e_shstrndx 0x0001- Não .

O leitor transforma essa linha em uma chamada:

pythonfields = struct.unpack_from("<HHIQQQIHHHHHH", data, 16)

A cadeia de formato é a figura gABI lida de esquerda para direita: duas meia-palavras, uma palavra, três valores de 8 bytes, uma palavra e seis meia-palavras. O leitor recusa um arquivo cuja magia, classe ou ordem de byte difere, e ele recusa e_shentsizeou e_phentsizeOs valores que não são 64 e 56. Uma largura errada faria cada compensação posterior errada, então o cheque fica no topo.

Títulos de secções e tabelas de duas cordas

Cada cabeçalho de secção é um Elf64_Shdrde 64 bytes (gABI Figura 4-8): sh_name- Não .sh_type- Não .sh_flags- Não .sh_addr- Não .sh_offset- Não .sh_size- Não .sh_link- Não .sh_info- Não .sh_addralign, e sh_entsizeA cadeia de formato é <IIQQQQIIQQ- Não .sh_nameé uma compensação para a secção cujo índice é e_shstrndx, e o nome termina no primeiro byte zero.e_shstrndxé 1, e o clang nomeou essa seção .strtab, por isso uma tabela serve tanto os nomes das secções como os nomes dos símbolos.

O leitor imprime os 11 cabeçalhos do objeto -O1:

textIdx Name             Type         Flags   Offset     Size       Addr Link Info Align
  0                  NULL                    0x0      0x0        0x0    0    0     0
  1 .strtab          STRTAB                0x368     0xbe        0x0    0    0     1
  2 .text            PROGBITS     AX        0x40     0x9f        0x0    0    0    16
  3 .rela.text       RELA         I        0x2a8     0x48        0x0   10    2     8
  4 .rodata          PROGBITS     A         0xe0     0x50        0x0    0    0     8
  5 .rela.rodata     RELA         I        0x2f0     0x78        0x0   10    4     8
  6 .rodata.str1.1   PROGBITS     AMS      0x130      0xa        0x0    0    0     1
  7 .comment         PROGBITS     MS       0x13a     0x2f        0x0    0    0     1
  8 .note.GNU-stack  PROGBITS              0x169      0x0        0x0    0    0     1
  9 .llvm_addrsig    LLVM_ADDRSIG E        0x368      0x0        0x0   10    0     1
 10 .symtab          SYMTAB                0x170    0x138        0x0    1    9     8

O índice 0 é o cabeçalho zero reservado. .textcontém 159 bytes de código no arquivo de compensação 0x40, logo após o cabeçalho, com bandeiras A(alocadas na memória) e X(executivo). .rodatacontém 80 bytes de dados apenas para leitura. .rodata.str1.1tem as cinco cordas de rótulo F\0D\0C\0B\0A\0, 10 bytes, com bandeiras M(mergível) e S- Não, não.

.commentDetém o selo de compiler de 47 bytes \0Apple clang version 17.0.0 (clang-1700.6.3.2)\0- Não ..llvm_addrsigtem tipo 0x6fff4c03, dentro da gama específica do sistema operacional SHT_LOOS- Não .SHT_HIOSque as reservas do gABI e do LLVM ELF.hA bandeira dele.EÉ o que éSHF_EXCLUDE, então um linker deixa cair.

O sh_linkE ...sh_infoAs colunas são os indicadores entre as tabelas. .rela.text- Não .sh_info2 e sh_link10: corre a secção 2 e toma os seus símbolos da secção 10. .symtab- Não .sh_link1, a tabela de cordas para os nomes dos símbolos e sh_info9, o índice do primeiro símbolo não local. sh_addré zero em cada linha porque um objeto não tem endereços ainda.

Duas regras gABI permitem que o leitor desenhe um mapa completo do arquivo. Seções "são contíguas e não se sobrepõem", e um arquivo "pode conter espaço inativo". O leitor classifica cada estrutura por offset e preenche os buracos:

text     0x0 +64    header   ELF header
    0x40 +159   section  .text
    0xdf +1     gap      inactive space
    0xe0 +80    section  .rodata
   0x130 +10    section  .rodata.str1.1
   0x13a +47    section  .comment
   0x169 +7     gap      inactive space
   0x170 +312   section  .symtab
   0x2a8 +72    section  .rela.text
   0x2f0 +120   section  .rela.rodata
   0x368 +190   section  .strtab
   0x426 +2     gap      inactive space
   0x428 +704   table    section header table

As três lacunas totalizam 10 bytes, e cada uma é alinhamento. .rodata- Não .sh_addralign8, então começa em 0xe0Em vez de0xdf- Não ..symtabTem alinhamento 8, por isso sete bytes ficam desutilizados depois .commentA tabela de cabeçalhos da secção em si está alinhada com 8.

Seções de tipo SHT_NOBITSNão ocupar bytes de arquivo em tudo, então o mapa os salta. Nada neste objeto tem esse tipo..bssnum programa ligado, e um leitor que confia.sh_size- Não. - Está lá o ficheiro.

Símbolos: ligação, tipo, secção, valor, tamanho

Um Elf64_Symé de 24 bytes (gABI Figura 4-18): st_name- Não .st_info- Não .st_other- Não .st_shndx- Não .st_value- Não .st_size, formato <IBBHQQ- Não .st_infoO gABI designa as ligações STB_LOCAL0 , STB_GLOBAL1, STB_WEAK2, e os tipos STT_NOTYPE0 , STT_OBJECT1, STT_FUNC2 , STT_SECTION3, STT_FILE4. Os dois bits mais baixos de st_othermanter a visibilidade,STV_DEFAULT0 aqui em todo.

textIdx            Value   Size Bind   Type    Section          Name
  0              0x0      0 LOCAL  NOTYPE  *UND*
  1              0x0      0 LOCAL  FILE    *ABS*            ledger.c
  2              0x0     20 LOCAL  OBJECT  .rodata          labels.rel
  3              0x0      0 LOCAL  SECTION .rodata          .rodata
  4              0x2      2 LOCAL  OBJECT  .rodata.str1.1   .L.str.1
  5              0x4      2 LOCAL  OBJECT  .rodata.str1.1   .L.str.2
  6              0x6      2 LOCAL  OBJECT  .rodata.str1.1   .L.str.3
  7              0x8      2 LOCAL  OBJECT  .rodata.str1.1   .L.str.4
  8              0x0      0 LOCAL  SECTION .rodata.str1.1   .rodata.str1.1
  9              0x0     16 GLOBAL FUNC    .text            percent_change
 10             0x10     46 GLOBAL FUNC    .text            grade
 11             0x40     31 GLOBAL FUNC    .text            total
 12             0x60     63 GLOBAL FUNC    .text            grade_label

O índice 0 é o símbolo reservado indefinido. st_shndxnome da seção que define o símbolo, com três valores reservados: SHN_UNDEF0 , SHN_ABS 0xfff1para o nome do ficheiro, e SHN_COMMON 0xfff2Os símbolos da secção têm um nome vazio, por issollvm-objdumpA regra gABI de que "todos os símbolos com STB_LOCAL ligando precedem os símbolos fracos e globais" aparece como as primeiras nove linhas, e .symtabregistados em 9 desh_infoPor isso.

Em um objeto realojávelst_valueé um deslocamento dentro da seção do símbolo, porque sh_addrO arquivo de compensação de uma função é, portanto, sh_offset + st_value - sh_addr, e o seu comprimento é st_size- Não .

pythonsection = elf.sections[symbol.st_shndx]
file_offset = section.sh_offset + (symbol.st_value - section.sh_addr)
code = elf.data[file_offset : file_offset + symbol.st_size]

Para opercent_changeÉ isso.0x40 + 0, e os 16 bytes são 85 ff 74 09 29 fe 6b c6 64 99 f7 ff c3 31 c0 c3Os tamanhos importam tanto quanto os arranques.gradetermina em .textcompensação0x3eOs dois bytes .66 90que seguem são um nopO compilador adicionou para que totalComeça em0x40O que é que é um símbolo de um "título" é um "título" que é um "título" que é um "título" que é um "título" que é um "título" que é um "título" que é um "título" que é um "título" que é um "título" que é um "título" que é um "título" que é um "título" que é um "título" que é um "título" que é um "título" que é um "título" que é um "título" que é um "título" que é um "título" que é um "título" que é um "título" que é um "título" que é um "título" que é um "título" que é um "título" que é um "título" que é um "título" que é um "título "título" que é um "título "título" que é um "título "título "st_sizeA lição 36 mascara-os antes de comparar uma reconstrução.

Relocalizações: o que significam os bytes zero

Um Elf64_Relaé de 24 bytes (gABI Figura 4-23): r_offset- Não .r_info, e um assinador_addend, formato <QQqO GABI se divide .r_infocom duas macros: ELF64_R_SYM(i)É o que éi >> 32E ...ELF64_R_TYPE(i)É o que éi & 0xffffffff- Num ficheiro realojável .r_offseté um deslocamento dentro da seção nomeada pela tabela sh_infoOs nomes e fórmulas dos tipos vêm do x86-64 psABI, Tabela 4.9.Sé o valor do símbolo, AO aditivo, PO local a ser parcheado e La entrada do PLT. R_X86_64_64É o que éS + A- Não .R_X86_64_PC32É o que éS + A - P, e R_X86_64_PLT32É o que éL + A - P- Não .

O objeto -O1 tem oito registos:

text  .rela.text       0x36 R_X86_64_PC32          .rodata+0x10
  .rela.text       0x84 R_X86_64_PC32          .rodata+0x24
  .rela.text       0x93 R_X86_64_PC32          .rodata-0x4
  .rela.rodata      0x0 R_X86_64_PC32          .rodata.str1.1
  .rela.rodata      0x4 R_X86_64_PC32          .L.str.1+0x4
  .rela.rodata      0x8 R_X86_64_PC32          .L.str.2+0x8
  .rela.rodata      0xc R_X86_64_PC32          .L.str.3+0xc
  .rela.rodata     0x10 R_X86_64_PC32          .L.str.4+0x10
  • Despejam o primeiro.0x36Está dentro .grade, que abrange 0x10- Não .0x3e- As instruções no0x33É o que é48 8d 0d 00 00 00 00, a leade um endereço RIP relativo, e o seu deslocamento de quatro bytes fica em 0x36O processador adiciona o deslocamento ao endereço da próxima instrução.0x3a- Não .

O compilador quer que o resultado seja .rodata + 0x14, onde uma tabela de cinco entradas de valores de 32 bits 1, 2, 3, 4, 4Então o valor armazenado deve ser(.rodata + 0x14) - 0x3aA fórmula .S + A - PcomP = 0x36dá.rodata + A - 0x36- Definição dos dois dados iguais .A = 0x14 - 4 = 0x10O adendo contém o offset-alvo menos os quatro bytes entre o deslocamento e o fim da instrução.

Essa tabela é como o clang compilou o switchem gradeO código calcula.score / 10com um multiplicador por 0x66666667O objeto -O1 ELF não mantém nenhum símbolo para a tabela..rodataMais um adicionado, e apenas a construção do arm64 da mesma fonte chama-o l_switch.table.gradeO segundo disco, em0x84em grade_label, pontos numa segunda tabela de valores de 8 bytes em .rodata + 0x28O terceiro, em0x93, pontos em .rodata + 0x0, o início de labels.rel- Não .

Os cinco ..rela.rodataregistos preencher labels.relCada entrada fica em compensação .4iE recebe o adicionado.4i- Então ...S + A - Preduz-se ao endereço da cadeia menos a base da tabela. grade_labellê uma entrada com movslqO primeiro registro chama o símbolo da seção.rodata.str1.1com adição 0 porque a cadeia FÉ o primeiro byte dessa seção..L.str.1- Não ..L.str.4, os rótulos locais do compilador para D- Não .C- Não .B, e A- Não .

A mesma fonte compila em três tabelas diferentes em três níveis de otimização. O leitor mostra a diferença sem um desmonte:

BuildWhere labels livesHow grade selectsHow grade_label reaches grade
-O0.data, five 8-byte pointers, five R_X86_64_64 recordsA jump table in .rodata: five R_X86_64_PC32 records pointing into .textA call with R_X86_64_PLT32 grade-0x4
-O1.rodata as labels.rel, five 32-bit relative entriesA value table at .rodata+0x14, one R_X86_64_PC32Inlined, no call
-O2Same as -O1Same as -O1Inlined, total grows to 140 bytes

-0 a 0switchÉ uma mesa de salto real cujas cinco entradas apontam para trás..text- Não .+0x91- Não .+0x8c- Não .+0x87- Não .+0x82, e +0x86- A chamada para ...grade- É o mesmo.-0x4A Comissão propõe que os Estados-Membros adoptem medidas de controlo para a aplicação da legislação comunitária.leaNo -O1, o optimizador viu que cada caso retorna uma constante e substituiu os saltos com uma tabela de resultados.

Títulos de Programação: O Visto do Carregador

Um objeto relocable não tem cabeçalhos de programa, então e_phnumUm executável ou um objeto compartilhado tem eles, e o carregador não lê nada mais.Elf64_Phdré de 56 bytes (gABI Figura 5-1): p_type- Não .p_flags- Não .p_offset- Não .p_vaddr- Não .p_paddr- Não .p_filesz- Não .p_memsz- Não .p_align, formato <IIQQQQQQO GABI fixa três regras para oPT_LOADEles aparecem em ascensão.p_vaddrordem, e um p_memszmaior do que p_fileszsignifica zero preenchimento.p_vaddré congruente com p_offsetModulo p_align- Não .

Cada alvo nesta faixa é um programa que você construiu, e nenhum linker neste Mac produz um executável Linux a partir de ledger.oEntão , o dispositivotiny-x86_64vem decode/fixtures/build_tiny_elf.py, que escreve 736 bytes com struct.packO arquivo contém um cabeçalho ELF, dois cabeçalhos de programa, um 36-byte._start, um 3 bytes .rodata, uma tabela de símbolos e seis cabeçalhos de secções._startescreveok\nNo Linux x86-64 ele é executado. Aqui é um ajuste para o caminho de cabeçalho do programa.

textProgram headers:
  PT_LOAD          r-x offset=0x0 vaddr=0x400000 filesz=0x2e0 memsz=0x2e0 align=0x1000
  PT_GNU_STACK     rw- offset=0x0 vaddr=0x0 filesz=0x0 memsz=0x0 align=0x10

entry 0x4000b0 is file offset 0xb0 through PT_LOAD

O single .PT_LOADmapeia todo o arquivo de 736 bytes em 0x400000O leitor mapeia um endereço para um arquivo compensado encontrando o PT_LOADque o contém e computação p_offset + (address - p_vaddr)Para o ponto de entrada que é0 + (0x4000b0 - 0x400000) = 0xb0, o início de .text- Não .

Este é o cálculo por trás da rea.address_to_file_offsetPara um alvo Ghidra, essa ferramenta retorna.{address, file_offset}O fornecedor do Hopper também volta .provider_file_offsetE ...image_base_file_offsetA ferramenta necessita de uma cópia original completa do ficheiro e o SHA-256 dessa cópia deve corresponder ao alvo.inspect_binary_layoutadiciona uma linha para manter: "Um endereço vinculado não é um endereço de tempo de execução". Uma carga executável independente de posição em uma base escolhida pelo kernel, e o offset de arquivo é a única coordenada estável.

Os cabeçalhos do programa também explicam por que um executável despojado ainda está em execução. Um linker pode remover a tabela de cabeçalhos de seção inteiramente, e e_shoffUma ferramenta que percorre secções não vê nada.

As mesmas ideias em Mach-O

A Apple não publica nenhuma especificação de prosa para Mach-O. Os cabeçalhos na fonte XNU são a especificação: EXTERNAL_HEADERS/mach-o/loader.h- Não .nlist.h, e reloc.h- O aparelho .ledger-arm64-O1.oÉ o mesmo.ledger.cconstruído com clang -target arm64-apple-macos -O1 -fno-asynchronous-unwind-tables -c- Não .

A.mach_header_64é 32 bytes: magic- Não .cputype- Não .cpusubtype- Não .filetype- Não .ncmds- Não .sizeofcmds- Não .flags- Não .reservedA magia é0xfeedfacf- Não .MH_MAGIC_64- Não .cputypeÉ o que é0x0100000c- Não .CPU_TYPE_ARM64- Não .filetypeé 1, MH_OBJECT- Não .ncmdsé 5 e sizeofcmdsé 616: os comandos de carga ocupam os 616 bytes após o cabeçalho, e cada um começa com {cmd, cmdsize}A bandeira .0x2000É o que éMH_SUBSECTIONS_VIA_SYMBOLS- Não .

textLoad commands:
  0 LC_SEGMENT_64                cmdsize=472 at 0x20
  1 LC_BUILD_VERSION             cmdsize=24 at 0x1f8
  2 LC_LINKER_OPTIMIZATION_HINT  cmdsize=16 at 0x210
  3 LC_SYMTAB                    cmdsize=24 at 0x220
  4 LC_DYSYMTAB                  cmdsize=80 at 0x238

LC_SEGMENT_640x19) é a resposta Mach-O para ambas as tabelas ELF.segment_command_64- Não .segname- Não .vmaddr- Não .vmsize- Não .fileoff- Não .filesize- Não .maxprot- Não .initprot- Não .nsects, e flagsEntão ...nsectsregistos de tipo section_64, 80 bytes cada, seguindo o mesmo comando.loader.hcomentar sobre MH_OBJECTDiz que as seções vivem "em um segmento sem nome sem tampão de segmento".fileoff648, filesize456, e cinco secções.

text  Idx Section                    Addr   Size Offset Reloff Nreloc Type
    1 __TEXT,__text               0x0   0xd0    648   1104      6 S_REGULAR
    2 __DATA,__const             0xd0   0x28    856   1152      5 S_REGULAR
    3 __TEXT,__cstring           0xf8    0xa    896      0      0 S_CSTRING_LITERALS
    4 __TEXT,__const            0x108   0x40    912      0      0 S_REGULAR
    5 __LD,__compact_unwind     0x148   0x80    976   1192      4 S_REGULAR

Cada secção nomeia o seu segmento-mãe em segname, por isso duas secções chamadas__constsão distintos porque um é__DATA,__constE o outro .__TEXT,__constAs mudanças são por secção atéreloffE ...nreloc, onde o ELF tem um separado .rela.Xsecção por alvo.

Cada um .relocation_infoÉ 8 bytes.r_addressUm campo de 32 bits segue, embalado comor_symbolnum(Bits 24), r_pcrel(1) r_length(2) r_extern(1) e r_type(4) Os seis __textOs registos são três pares deARM64_RELOC_PAGE21E ...ARM64_RELOC_PAGEOFF12, um par por mesa de referência.adrp- E mais .addfaz o que um x86-64 lea- É verdade, por isso, cada referência precisa de dois registos.__DATA,__constOs registos sãoARM64_RELOC_UNSIGNEDcomr_length3, então cada um corre um punteiro absoluto de 8 bytes.

A -O1 a construção do arm64 manteve-selabelscomo ponteiros simples onde a construção x86-64 fez uma tabela relativa.

LC_SYMTAB(0x2) é válido symoff1240, nsyms17 anos.stroff1512, strsize176, e 1512 mais 176 é 1688, o tamanho do arquivo.nlist_64é de 16 bytes: n_strx- Não .n_type- Não .n_sect- Não .n_desc- Não .n_value- Não .n_typeÉ um campo de trabalho.N_TYPE(mascareta)0x0e) igual a N_SECT(0xe) significa definido na secção cujo ordinal baseado em 1 é n_sect- Não .N_EXT(0x01) marca um símbolo externo.

nlist_64Não tem campo de tamanho._percent_changeO leitor toma o seguinte endereço do símbolo na mesma secção, _grade- Não .0x18. Os símbolos C têm um liderança _Aqui, então a função é _percent_change- Não .

LC_DYSYMTAB(0xb) registra a mesma regra local-primeira que a ELF sh_info- Não .ilocalsym0 , nlocalsym13, iextdefsym13, nextdefsym4. LC_BUILD_VERSION(0x32) registos platform1 (macOS), minos26.0 e sdk26.2 embalado XXXX.YY.ZZO comando é por isso que o arm64 fixture é comprometido como bytes. Uma construção em um Mac com outro SDK produz um arquivo diferente.

O leitor é o que eu quero.compare_formatscoloca os dois arquivos lado a lado:

IdeaELF, ledger-x86_64-O1.oMach-O, ledger-arm64-O1.o
File identitye_ident 7f 45 4c 46, class 64, data LSBmagic 0xfeedfacf
Machinee_machine 62, EM_X86_64cputype 0x100000c, CPU_TYPE_ARM64
File kinde_type 1, ET_RELfiletype 1, MH_OBJECT
Where the tables aree_shoff 0x428, 11 section headers5 load commands, 616 bytes after the header
The code.text at 0x40, size 0x9f, flags AX__TEXT,__text at 648, size 0xd0, in the unnamed segment
Symbol names.symtab (section 10), names in .strtab through sh_link 1LC_SYMTAB symoff 1240, nsyms 17, names at stroff 1512
Symbol sizest_size: percent_change is 16 bytesNo field: next symbol minus this one, 24 bytes
Relocations.rela.text, 3 records, R_X86_64_PC32__text reloff 1104, 6 records, PAGE21 and PAGEOFF12
Function bytespercent_change: 16 bytes at 0x40_percent_change: 24 bytes at 0x288

Rea lê Mach-O através de inspect_macho, que correotool -h -l- Não .nm -gjU- Não .dyld_info -imports -exports, e dwarfdump --uuidatravés dexcrunO teu leitor não precisa de nada, e lê o mesmo.symoffE ...stroffQue ...otool -lAs impressões digitais.open_binaryO instrumento de rea detecta ELF, MZ, PE e Mach-O fino ou universal dos bytes mágicos.e_machineIsso é os primeiros 20 bytes do que você construiu aqui. para um arquivo sem cabeçalho, você dáformat: raw-binaryO provedor Ghidra recusa um binário universal e pede uma fatia magra.0xcafebabe- Não .

Construí-lo

Execute o leitor de biblioteca padrão:

bashcd phases/13-tools-and-protocols/32-program-anatomy-elf-from-scratch
python3 code/main.py

A demonstração diz:ledger-x86_64-O1.oe imprime seu cabeçalho, seções, símbolos, relocalizações e mapa de offset de arquivo.ledger-arm64-O1.o, e imprime a tabela de comparação.tiny-x86_64- Linhas esperadas:

textpercent_change: 16 bytes at file offset 0x40: 85 ff 74 09 29 fe 6b c6 64 99 f7 ff c3 31 c0 c3
grade: 46 bytes at file offset 0x50: 48 63 c7 48 69 c0 67 66 66 66 ...
entry 0x4000b0 is file offset 0xb0 through PT_LOAD

Passe um caminho para ler um arquivo: python3 code/main.py code/fixtures/ledger-x86_64-O0.oO leitor escolhe ELF ou Mach-O dos primeiros quatro bytes.

Leia code/main.pyna ordem seguinte:

  1. ElfFile._read_header()verifica a magia, a classe, a ordem de byte e os tamanhos de entrada, e depois desembaga o cabeçalho.
  2. ElfFile._read_sections()E ...read_cstring()ler a tabela de cabeçalhos de secções e resolver nomes através de e_shstrndx- Não .
  3. ElfFile._read_symbols()Divisõesst_info, resolve nomes através de sh_link, e mantém os índices da secção reservada legíveis.
  4. ElfFile._read_relocations()anda a cada vez.SHT_RELAsecção, divisões r_info, e os tipos de nomes do quadro 4.9.
  5. ElfFile.function_span()E ...function_bytes()transformar um símbolo em um intervalo de arquivos e os bytes nele.
  6. ElfFile.file_offset_map()classifica todas as estruturas por compensação e preenche o espaço inativo.
  7. ElfFile._read_program_headers()E ...address_to_file_offset()Manutenção de executáveis.
  8. MachOFilelê o cabeçalho, os comandos de carga,LC_SEGMENT_64com as suas secções, LC_SYMTAB- Não .LC_BUILD_VERSION, e realocações por secção.
  9. compare_formats()Nomes da mesma ideia em ambos os arquivos com os valores leídos de cada um.

Usá-lo

Importa o leitor das aulas posteriores com from main import ElfFileDepois de adicionar a lição codediretório de sys.pathA lição 33 pede-lhe .function_bytesE ...relocations_for(".text")A lição 35 registra a sua saída como evidência observada. a lição 36 pede que os offsets de realocação sejam mascarados antes de uma comparação em byte.

Compile o seu próprio C em um objeto com o mesmo clang -target x86_64-unknown-linux-gnu -cAdicionar um nome de realocação apenas quando a tabela 4.9 x86-64 psABI o enumera. Adicionar um tipo de seção apenas quando o gABI ou a cadeia de ferramentas produtor documentá-lo. Quando o leitor encontra um valor para o qual não tem nome, ele imprime o número em hexadecimal em vez de adivinhar.

Laboratório Interativo

Laboratório A: o cabeçalho à mão

  • Docodediretório, Python aberto:
bashcd phases/13-tools-and-protocols/32-program-anatomy-elf-from-scratch/code
python3 -q
  • Correr .
pythonimport struct
from main import ElfFile, FIXTURES
data = (FIXTURES / "ledger-x86_64-O1.o").read_bytes()
data[:4], data[4], data[5]
struct.unpack_from("<HHIQQQIHHHHHH", data, 16)
elf = ElfFile(data)
elf.header.e_shoff, elf.header.e_shnum, elf.sections[elf.header.e_shstrndx].name

O show de tuplos .e_type1, e_machine62, e e_shoff1064, que é 0x428A última linha mostra que a secção 1.strtab, tem os nomes das secções.

Laboratório B: o quadro de valores por trás do interruptor

pythonrodata = elf.section_bytes(elf.section(".rodata"))
[int.from_bytes(rodata[i : i + 4], "little") for i in range(0x14, 0x28, 4)]
[int.from_bytes(rodata[i : i + 8], "little") for i in range(0x28, 0x50, 8)]
[r for r in elf.relocations_for(".text") if r.symbol == ".rodata"]

As duas tabelas são ledas .1, 2, 3, 4, 4Verifique-os contra .ledger.c- Não .grade(65)Computação65 / 10 - 6 = 0e lê a entrada 0, que é 1, o valor de case 6- Não .grade(100)O valor é 4 , o valor compartilhado por case 10E ...case 9- Não .

Laboratório C: três construções de uma fonte

pythonfor level in ("O0", "O1", "O2"):
    obj = ElfFile.from_path(FIXTURES / f"ledger-x86_64-{level}.o")
    print(level, [s.name for s in obj.sections if s.name in (".data", ".rodata")])
    print(level, sorted({r.type_name for r in obj.relocations}))
    print(level, {s.name: s.st_size for s in obj.symbols if s.type == "FUNC"})

O objeto -O0 tem um .dataOs objetos -O1 e -O2 não têm.dataE um tipo.totalé 66 bytes em -O0, 31 em -O1, e 140 em -O2.

Laboratório D: a mesma função em Mach-O

pythonfrom main import MachOFile
macho = MachOFile.from_path(FIXTURES / "ledger-arm64-O1.o")
[(c.name, c.cmdsize) for c in macho.load_commands]
macho.function_span("_percent_change")
macho.function_bytes("_percent_change")[:4].hex()

O espaço é de 24 bytes no arquivo de 648 offset. Os primeiros quatro bytes são a0000034, a forma de instrução de pequeno porte .0x340000a0, que llvm-objdump -dimpressões como cbz w0, 0x14É o que eu quero .before == 0O mesmo teste que o x86-64 codifica como 85 ff 74 09- Não .

Laboratório de Prática

Extenda o leitor para um programa ligado. Em uma máquina Linux x86-64, construa clang -O1 -shared -fPIC code/fixtures/ledger.c -o libledger.soE ...clang -O1 code/fixtures/ledger.c -o ledger_maincom um mainadicionar, e depois executar o leitor em ambos.

Requisitos:

  • Leia .dynsymcom o mesmo Elf64_Sym- a disposição, desde SHT_DYNSYMÉ 11 e o registro é inalterado.
  • Leia .dynamicComo Elf64_Dynregistos de d_tagE ...d_val, e nome DT_NEEDED- Não .DT_STRTAB- Não .DT_SYMTAB- Não .DT_RELA, e DT_JMPRELda página gABI "Seção dinâmica".
  • Lista os símbolos não definidos com st_shndx SHN_UNDEFAs importações e as importaçõesR_X86_64_JUMP_SLOTregistos em .rela.pltcomo os sites de chamadas que os alcançam.
  • Mapa de cada umPT_LOADSegmento para as seções que contém, usando a regra gABI de que "um segmento de arquivo objeto contém uma ou mais seções".
  • Prova com address_to_file_offsetque os bytes de cada função definida no objeto compartilhado são os bytes function_bytes- Não.
  • Captura .llvm-objdump -h -t -r -pE ...llvm-objdump -Ttexto para ambos os arquivos em code/fixtures/e comparar o leitor com eles em testes.

O sucesso significa que a lista de importação, o mapa de segmento e cada intervalo de funções concordam com a saída da ferramenta capturada, sem nenhuma execução de ferramentas durante os testes.

Lição Artifacto

Esta lição produzoutputs/skill-program-anatomy-elf-from-scratch.md. Use-o com Claude, Cursor, Codex, OpenClaw, Hermes ou qualquer agente que leia SKILL.md. A habilidade transforma um objeto compilado ou executável que o usuário construiu em um relatório de layout. O relatório dá a faixa de arquivos de cada estrutura, símbolo e realocação, verificada contra a saída da ferramenta capturada.

Verifique

Execute a demonstração e a suíte determinista:

bashcd phases/13-tools-and-protocols/32-program-anatomy-elf-from-scratch
python3 code/main.py
python3 -m unittest discover -s code/tests -v

A suíte prova:

  • Cada secção, nome, tamanho, endereço e tipo coincide com o capturado.llvm-objdump -htexto para os três objetos x86-64 e o pequeno executável
  • Cada valor de símbolo, vinculação, tipo, seção, tamanho e nome correspondem ao capturado llvm-objdump -ttexto
  • Cada deslocamento de compensação, nome de tipo, símbolo e adendo coincide com o capturado llvm-objdump -rtexto, incluindo R_X86_64_PLT32E ...R_X86_64_64em -O0
  • Os dois cabeçalhos do programa detiny-x86_64- A par dos capturados .llvm-objdump -ptexto, e os objetos não têm nenhum
  • percent_changeé 16 bytes em arquivo de compensação 0x40com os bytes esperados, e cada função está dentro .text
  • O mapa de offset de arquivo cobre cada byte de cada fixação uma vez, com as três lacunas de alinhamento do objeto -O1 em seus lugares
  • o ponto de entrada de tiny-x86_64mapas para arquivos de compensação 0xb0através dePT_LOAD, e um objeto realojável recusa o mapeamento de endereços
  • O cabeçalho Mach-O, cada comando de carga, o segmento, as cinco seções,LC_SYMTAB, e LC_BUILD_VERSION- A par dos capturados .--private-headerstexto
  • Os símbolos Mach-O e as 15 realocações correspondem aos capturados .llvm-objdump- o texto e_percent_changeé 24 bytes no offset 648
  • Uma mágica errada, uma classe de 32 bits, um arquivo de grande endea, uma tabela truncada, um binário universal e um arquivo de fonte C todos aumentam.FormatError
  • build_tiny_elf.pyReproduza o comprometido tiny-x86_64Byte por byte
  • o módulo de figuras site/figures-re-32.jsCarrega os bytes exatos de ledger-x86_64-O1.o

Modos de falha de produção

FailureWhat the weak reading reportsWhat the reader must prove
st_value used as a file offset"percent_change is at 0"Offset is sh_offset + st_value - sh_addr, here 0x40
Padding counted as code"grade is 48 bytes"st_size is 46 and the 66 90 after it belongs to no symbol
Section names read from the wrong tableGarbage names or an exceptione_shstrndx names the table, here section 1
sh_size trusted for SHT_NOBITSA read past the end of the file.bss has no file bytes and the map skips it
Zero bytes read as the operand"lea 0(%rip)"The .rela.text record at 0x36 names .rodata+0x10
Addend read as the target offset"the table starts at 0x10"S + A - P with the instruction end gives 0x14
Sections walked on a stripped executable"the file has no code"PT_LOAD maps every address the loader uses
A linked address treated as a runtime addressA wrong offset under a random baseThe file offset from PT_LOAD is the stable coordinate
A Mach-O section looked up by sectname aloneTwo __const sections collapse into onesegname,sectname is the name
A size field expected in nlist_64An exception or zero bytesThe next symbol in the same section ends the function
A universal binary read as one Mach-O"magic 0xcafebabe is unknown"Refuse it and ask for a thinned slice
The arm64 object rebuilt on another Mac"the fixture changed"LC_BUILD_VERSION records the SDK, so the committed bytes are the fixture

Regra de funcionamento

Leia o cabeçalho em que você pode confiar, siga os offsets que ele possui e nomee cada campo da especificação que o define. Mantenha a saída da ferramenta comparada como um recurso comprometido, para que a comparação seja executada sem a ferramenta. Quando um valor não tem nome na especificação, imprima o número. O leitor que você pode verificar byte por byte é aquele que cada lição posterior baseia-se.

Mais leitura

This free lesson is part of the AI Engineering from Scratch curriculum. Read the full explanation, run the lesson code, and verify the result in the interactive reader or from the repository source.

Browse the complete course catalog or open this lesson on GitHub.