Anatomia do Programa: Um Leitor ELF do zero
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)
| Source | What it allows | Conditions |
|---|---|---|
| 17 U.S.C. 1201(f) | Circumventing an access control to analyze a program | Lawful copy, interoperability purpose, information not readily available, no infringement |
| Directive 2009/24/EC Art. 5(3) | Observing, studying, and testing a running program | A right to use the copy, acts within normal loading and running |
| Directive 2009/24/EC Art. 6 | Decompilation | Licensee, interoperability purpose, information not readily available, confined to the needed parts |
| Sega v. Accolade | Disassembly as fair use | The only way to reach the ideas and functional elements, a legitimate reason |
| Sony v. Connectix | Intermediate copies during reverse engineering | A 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 8O í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 tableAs 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_labelO í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 abrange0x10- Não .0x3e- As instruções no0x33É o que é48 8d 0d 00 00 00 00, aleade um endereço RIP relativo, e o seu deslocamento de quatro bytes fica em0x36O 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:
| Build | Where labels lives | How grade selects | How grade_label reaches grade |
|---|---|---|---|
| -O0 | .data, five 8-byte pointers, five R_X86_64_64 records | A jump table in .rodata: five R_X86_64_PC32 records pointing into .text | A call with R_X86_64_PLT32 grade-0x4 |
| -O1 | .rodata as labels.rel, five 32-bit relative entries | A value table at .rodata+0x14, one R_X86_64_PC32 | Inlined, no call |
| -O2 | Same as -O1 | Same as -O1 | Inlined, 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_LOADO 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 0x238LC_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_REGULARCada 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:
| Idea | ELF, ledger-x86_64-O1.o | Mach-O, ledger-arm64-O1.o |
|---|---|---|
| File identity | e_ident 7f 45 4c 46, class 64, data LSB | magic 0xfeedfacf |
| Machine | e_machine 62, EM_X86_64 | cputype 0x100000c, CPU_TYPE_ARM64 |
| File kind | e_type 1, ET_REL | filetype 1, MH_OBJECT |
| Where the tables are | e_shoff 0x428, 11 section headers | 5 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 1 | LC_SYMTAB symoff 1240, nsyms 17, names at stroff 1512 |
| Symbol size | st_size: percent_change is 16 bytes | No 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 bytes | percent_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.pyA 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_LOADPasse 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:
ElfFile._read_header()verifica a magia, a classe, a ordem de byte e os tamanhos de entrada, e depois desembaga o cabeçalho.ElfFile._read_sections()E ...read_cstring()ler a tabela de cabeçalhos de secções e resolver nomes através dee_shstrndx- Não .ElfFile._read_symbols()Divisõesst_info, resolve nomes através desh_link, e mantém os índices da secção reservada legíveis.ElfFile._read_relocations()anda a cada vez.SHT_RELAsecção, divisõesr_info, e os tipos de nomes do quadro 4.9.ElfFile.function_span()E ...function_bytes()transformar um símbolo em um intervalo de arquivos e os bytes nele.ElfFile.file_offset_map()classifica todas as estruturas por compensação e preenche o espaço inativo.ElfFile._read_program_headers()E ...address_to_file_offset()Manutenção de executáveis.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.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
- Do
codediretó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].nameO 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 mesmoElf64_Sym- a disposição, desdeSHT_DYNSYMÉ 11 e o registro é inalterado. - Leia
.dynamicComoElf64_Dynregistos ded_tagE ...d_val, e nomeDT_NEEDED- Não .DT_STRTAB- Não .DT_SYMTAB- Não .DT_RELA, eDT_JMPRELda página gABI "Seção dinâmica". - Lista os símbolos não definidos com
st_shndxSHN_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 um
PT_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 bytesfunction_bytes- Não. - Captura .
llvm-objdump -h -t -r -pE ...llvm-objdump -Ttexto para ambos os arquivos emcode/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 -vA 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, incluindoR_X86_64_PLT32E ...R_X86_64_64em -O0 - Os dois cabeçalhos do programa de
tiny-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ção0x40com 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ção0xb0atravé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, eLC_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 comprometidotiny-x86_64Byte por byte- o módulo de figuras
site/figures-re-32.jsCarrega os bytes exatos deledger-x86_64-O1.o
Modos de falha de produção
| Failure | What the weak reading reports | What 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 table | Garbage names or an exception | e_shstrndx names the table, here section 1 |
sh_size trusted for SHT_NOBITS | A 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 address | A wrong offset under a random base | The file offset from PT_LOAD is the stable coordinate |
A Mach-O section looked up by sectname alone | Two __const sections collapse into one | segname,sectname is the name |
A size field expected in nlist_64 | An exception or zero bytes | The 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
- System V gABI, chapter 4: Object Filese o ELF Header- Não .Sections- Não .Symbol Table, e RelocationPáginas
- System V gABI, chapter 5: Program HeaderE ...Dynamic Linking
- System V ABI, AMD64 Architecture Processor Supplement, Tabela 4.9 Tipos de realocação
- Apple XNU
loader.h- Não .nlist.h, ereloc.h - 17 U.S.C. 1201E ...Directive 2009/24/EC
- Sega v. Accolade, 977 F.2d 1510E ...Sony v. Connectix, 203 F.3d 596
- rea 6.5.0
docs/binary-diagnostics.mdE ...docs/ghidra-provider.md
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.