A Aldeia Numaboa ancestral ainda está disponível para visitação. É a versão mais antiga da Aldeia que eu não quis simplesmente descartar depois de mais de 10 milhões de pageviews. Como diz a Sirley, nossa cozinheira e filósofa de plantão: "Misericórdia, ai que dó!"

Se você tiver curiosidade, o endereço é numaboa.net.br.

Leia mais...

Informática Numaboa - Referências

O formato PE

Sab

11

Abr

2009


11:01

(10 votos, média 5.00 de 5) 


Cabeçalho opcional

Cabeçalho MZ do DOS
Fragmento (stub) do DOS
Cabeçalho do Arquivo
Cabeçalho Opcional
Diretório de Dados
Cabeçalhos das Seções
Seção 1
Seção 2
...
Seção n

Imediatamente após o cabeçalho do arquivo vem o cabeçalho opcional que, apesar do nome, está sempre presente. Este cabeçalho contém informações de como o arquivo PE deve ser tratado. Os componentes do cabeçalho opcional são os seguintes:

  • Magic
  • MajorLinkerVersion e MinorLinkerVersion
  • Tamanho do Código, Segmento de Dados e Segmento BSS
  • AddressOfEntryPoint - O Ponto de Entrada do Código do Executável
  • Base do Código e Base dos Dados
  • Base da Imagem
  • Alinhamento
  • Versão do Sistema Operacional
  • Versão do Binário
  • Versão do Subsistema
  • Versão do Win32
  • Tamanho da Imagem
  • Tamanho dos Cabeçalhos
  • CheckSum
  • Subsistema NT
  • Características de DLL
  • Tamanho da Reserva de Pilha (StackReserve)
  • Loader Flags
  • Número e Tamanho dos RVA

Magic

O primeiro word de 16 bits do cabeçalho opcional é o 'Magic'. Em todos os arquivos PE que analisei até hoje, o valor encontrado sempre foi 010B.

Offset 01234 56789 ABCDE F
0000 00C0 50 45 00 00 4C010300 A377553C 00000000
0000 00D0 00000000 E0000F01 0B 01        

MajorLinkerVersion e MinorLinkerVersion

O próximo componente do cabeçalho opcional, composto de 2 bytes, reflete as versões Maior e Menor do linker utilizado. Estes valores, novamente, não são confiáveis e nem sempre refletem apropriadamente a versão do linker. Muitos linkers nem mesmo utilizam estes campos. Aliás, se não se tem a mínima idéia de qual linker tenha sido utilizado, qual é a vantagem de conhecer a versão?

Offset 01234 56789 ABCDE F
0000 00C0 50 45 00 00 4C010300 A377553C 00000000
0000 00D0 00000000 E0000F01 0B 01 050C     

Tamanho do Código, Segmento de Dados e Segmento BSS

Os 3 valores de 32 bits seguintes referem-se ao Tamanho do Código Executável ('SizeOfCode'), ao Tamanho dos Dados Inicializados ('SizeOfInitializedCode') e ao Tamanho dos Dados não Inicializados ('SizeOfUninitializedCode'). O tamanho dos dados inicializados também é conhecido como Segmento de Dados (Data Segment) e o tamanho dos dados não inicializados é conhecido como Segmento BSS (BSS Segment). Estes dados, uma vez mais, não são confiáveis! Por exemplo: o segmento de dados pode estar dividido em vários segmentos por ação do compilador ou do linker. O melhor que se tem a fazer é inspecionar as seções para ter uma idéia mais aproximada dos tamanhos.

Offset 01234 56789 ABCDE F
0000 00C0 50 45 00 00 4C010300 A377553C 00000000
0000 00D0 00000000 E0000F01 0B 01 050C 00 02 00 00
0000 00E0 00 0E 00 00 00 00 00 00          

Ponto de entrada do código executável

O próximo valor de 32 bits é um RVA. Se você tiver dúvidas sobre RVAs, NÃO CONTINUE. Volte para a página anterior e leia com atenção as Considerações sobre RVAs. A partir deste ponto, se você não estiver familiarizado com as ditas cujas... vai perder o passo!

Este RVA é o offset para o Ponto de Entrada do Código ('AddressOfEntryPoint'), ou seja, é onde a execução do nosso binário, MAPEADO NA MEMÓRIA, realmente começa.

Offset 01234 56789 ABCDE F
0000 00C0 50 45 00 00 4C010300 A377553C 00000000
0000 00D0 00000000 E0000F01 0B 01 050C 00 02 00 00
0000 00E0 00 0E 00 00 00 00 00 00 00 10 00 00     

Invertendo os bytes, obtemos o RVA do Endereço do Ponto de Entrada: 00 10 00 00 -> 00 00 10 00. Este valor (1000) é o que será adicionado ao endereço base quando nosso programa for mapeado na MEMÓRIA. Imagine que, ao ser executado, o executável tenha sido mapeado na memória a partir do endereço 40000. Qual será o ponto de entrada do código? Simples: 40000 + 1000 = 41000. Entendeu agora porque o valor deste campo é um RVA?

Base do Código e Base dos Dados

Logo após o importantíssimo Ponto de Entrada encontram-se dois valores de 32 bits, o 'BaseOfCode' (base do código) e o 'BaseOfData' (base dos dados), ambos também RVAs. Infelizmente os dois também perdem importância (como tantos outros campos) por que a informação obtida através da análise das seções é muito mais confiável.

Não existe uma base de dados não inicializados porque, por não serem inicializados, não há necessidade de incluir esta informação na imagem.

Offset 01234 56789 ABCDE F
0000 00C0 50 45 00 00 4C010300 A377553C 00000000
0000 00D0 00000000 E0000F01 0B 01 050C 00 02 00 00
0000 00E0 00 0E 00 00 00 00 00 00 00 10 00 00 00100000
0000 00F0 00200000

Base da Imagem

Segue uma entrada de um valor de 32 bits que indica o endereço de mapeamento preferencial, chamado de endereço linear e correspondendo à 'BaseImage'. No momento da execução, se este endereço de memória estiver vago, o binário inteiro (incluindo os cabeçalhos) será transferido para lá. Este é o endereço, sempre um múltiplo de 64, para onde o binário é remanejado pelo linker. Se o endereço estiver disponível, o carregador (loader) não precisará remanejar o arquivo, o que representa um ganho no tempo de carregamento.

O endereço preferido de mapeamento não pode ser utilizado se outra imagem já tiver sido mapeada para este endereço (uma colisão de endereços, a qual ocorre com alguma frequência quando se carrega várias DLLs que são remanejadas para o default do linker) ou se a memória em questão estiver sendo usada para outros fins (stack, malloc(), dados não inicializados, etc). Nestes casos, a imagem precisa ser transferida para algum outro endereço (veja 'diretório de remanejamento' logo a seguir). Este fato gera consequências posteriores se a imagem pertencer a uma DLL por que, neste caso, as importações casadas ("bound imports") deixam de ser válidas e há a necessidade de efetuar correções nos binários que utilizam estas DLLs - veja também em 'diretório de remanejamento' a seguir.

Offset 01234 56789 ABCDE F
0000 00C0 50 45 00 00 4C010300 A377553C 00000000
0000 00D0 00000000 E0000F01 0B 01 050C 00 02 00 00
0000 00E0 00 0E 00 00 00 00 00 00 00 10 00 00 00100000
0000 00F0 00200000 00 40 00 00

Alinhamento

Os dois valores de 32 bits seguintes são os alinhamentos das seções do arquivo PE na RAM ('SectionAlignment', quando a imagem estiver carregada na memória) e no arquivo em disco ('FileAlignment'). Geralmente ambos valores são 32, ou então FileAlignment (alinhamento de arquivo) é 512 e SectionAlignment (alinhamento de seções) é 4096. As seções serão vistas posteriormente.

Offset 01234 56789 ABCDE F
0000 00C0 50 45 00 00 4C010300 A377553C 00000000
0000 00D0 00000000 E0000F01 0B 01 050C 00 02 00 00
0000 00E0 00 0E 00 00 00 00 00 00 00 10 00 00 00100000
0000 00F0 00200000 00 40 00 00 00 10 00 00 00 02 00 00

No nosso exemplo, o alinhamento de seções é 4096 (0010 0000 -> inverso 0000 1000 -> 4096 decimal) e o alinhamento de arquivo é 512 (0002 0000 -> inverso 0000 0200 -> 512 decimal).

Versão do Sistema Operacional

Os dois valores seguintes são de 16 bits e referem-se à versão esperada do sistema operacional ('MajorOperatingSystemVersion' e 'MinorOperatingSystemVersion'). Esta informação da versão é apenas para o sistema operaconal, por exemplo NT ou Win98, ao contrário da versão do sub-sistema, por exemplo Win32. Geralmente esta informação não é fornecida ou está errada. Aparentemente o carregador (loader) não faz uso da mesma, portanto...

Offset 01234 56789 ABCDE F
0000 00C0 50 45 00 00 4C010300 A377553C 00000000
0000 00D0 00000000 E0000F01 0B 01 050C 00 02 00 00
0000 00E0 00 0E 00 00 00 00 00 00 00 10 00 00 00100000
0000 00F0 00200000 00 40 00 00 00 10 00 00 00 02 00 00
0000 0100 04000000

Versão do Binário

Os dois valores de 16 bits seguintes fornecem a versão do binário ('MajorImageVersion' e 'MinorImageVersion'). Muitos linkers não fornecem dados corretos e uma grande parte dos programadores nem se dá ao trabalho de fornecê-los. O melhor é se fiar na versão dos recursos (resource), contanto que exista.

Offset 01234 56789 ABCDE F
0000 00C0 50 45 00 00 4C010300 A377553C 00000000
0000 00D0 00000000 E0000F01 0B 01 050C 00 02 00 00
0000 00E0 00 0E 00 00 00 00 00 00 00 10 00 00 00100000
0000 00F0 00200000 00 40 00 00 00 10 00 00 00 02 00 00
0000 0100 04000000 00000000

Versão do Sub-sistema

Os próximos 2 words de 16 bits são para a versão do sub-sistema esperado ('MajorSubsystemVersion' e 'MinorSubsystemVersion'). Esta versão deveria ser Win32 ou POSIX, por que os programas de 16 bits ou os do OS/2 obviamente não estão em formato PE.

Esta versão de subsistema deve ser fornecida corretamente porque ela É checada e usada:

  • Se o aplicativo for um do tipo Win32-GUI, tiver que rodar em NT4 e a versão do sub-sistema não for 4.0, as caixas de diálogo não terão o estilo 3D e alguns outros aspectos terão a aparência do "estilo antigo". Isto porque o aplicativo acaba sendo rodado no NT 3.51, o qual possui o program manager ao invés do explorer, etc, e o NT 4.0 tentará imitar o 3.51 da melhor maneira possível.
  • Idem para Win98 e WinMe. O aplicativo indica Win98 e o sistema da máquina é WinMe, então o WinMe tenta de tudo para imitar o Win98...
Offset 01234 56789 ABCDE F
0000 00C0 50 45 00 00 4C010300 A377553C 00000000
0000 00D0 00000000 E0000F01 0B 01 050C 00 02 00 00
0000 00E0 00 0E 00 00 00 00 00 00 00 10 00 00 00100000
0000 00F0 00200000 00 40 00 00 00 10 00 00 00 02 00 00
0000 0100 04000000 00000000 04 00 00 00 00000000

Versão do Win32

Só Deus sabe para é que serve este próximo valor de 32 bits. Está sempre zerado (veja acima).

Tamanho da Imagem

Este valor de 32 bits indica a quantidade de memória necessária para abrigar a imagem, em bytes ('SizeOfImage'). É a soma do comprimento de todos os cabeçalhos e seções, se estiverem alinhados de acordo com o 'SectionAlignement'. Indica para o carregador quantas páginas serão necessárias para carregar completamente a imagem.

Offset 01234 56789 ABCDE F
0000 00C0 50 45 00 00 4C010300 A377553C 00000000
0000 00D0 00000000 E0000F01 0B 01 050C 00 02 00 00
0000 00E0 00 0E 00 00 00 00 00 00 00 10 00 00 00100000
0000 00F0 00200000 00 40 00 00 00 10 00 00 00 02 00 00
0000 0100 04000000 00000000 04 00 00 00 00000000
0000 0110 00 50 00 00

No nosso exemplo, obedecendo o alinhamento de seções de 4096 (veja acima), são requeridos 20.480 bytes para abrigar a imagem do executável na memória. Basta calcular: 0050 0000 -> inverso 0000 5000 -> 20.480 decimal.

Tamanho dos Cabeçalhos

O próximo valor de 32 bits é o tamanho de todos os cabeçalhos, incluindo os diretórios de dados e os cabeçalhos das seções ('SizeOfHeaders'). Representa o offset do início do arquivo até os dados (raw data) da primeira seção.

Offset 01234 56789 ABCDE F
0000 00C0 50 45 00 00 4C010300 A377553C 00000000
0000 00D0 00000000 E0000F01 0B 01 050C 00 02 00 00
0000 00E0 00 0E 00 00 00 00 00 00 00 10 00 00 00100000
0000 00F0 00200000 00 40 00 00 00 10 00 00 00 02 00 00
0000 0100 04000000 00000000 04 00 00 00 00000000
0000 0110 00 50 00 00 00 04 00 00 00000000

No nosso exemplo, o offset do início do arquivo até os dados propriamente ditos é de 0000 0400 que, em decimal, corresponde a 1024 bytes.

CheckSum

Segue-se o valor de 32 bits do 'CheckSum'. O valor do checksum, para as versões atuais do NT, só é checado se a imagem for um driver NT (o driver não carregará se o checksum não estiver correto). Para outros tipos de binários o checksum não precisa ser fornecido e pode ser 0.

O algoritmo para calcular o checksum é propriedade da Microsoft e o pessoal da MS não entrega o ouro. No entanto, diversas ferramentas do Win32 SDK calculam e/ou inserem um checksum válido. Além disto, a função CheckSumMappedFile(), que faz parte da imagehelp.dll, também faz o serviço completo.

A função do checksum é a de evitar que binários "bichados", que vão dar pau de qualquer forma, sejam carregados - e um driver com pau acaba em BSOD, portanto, é melhor nem carregar.

No nosso exemplo, que não é para NT, o valor está zerado (veja acima).

Subsistema NT

O próximo valor de 16 bits, o 'Subsystem', indica em qual subsistema do NT a imagem deve rodar:

NomeValorSignificado
IMAGE_SUBSYSTEM_NATIVE1O binário não precisa de um subsistema. É usado para drivers.
IMAGE_SUBSYSTEM_WINDOWS_GUI2A imagem é um binário Win32 gráfico. Ainda pode abrir um console com AllocConsole(), porém não abre automaticamente no startup.
IMAGE_SUBSYSTEM_WINDOWS_CUI3O binário é um Win32 de console. Receberá um console no startup (default) ou herda um console (parent's console).
IMAGE_SUBSYSTEM_OS2_CUI5O binário é um OS/2 de console. Os binários OS/2 estarão em formato OS/2, portanto, este valor raramente será encontrado num arquivo PE.
IMAGE_SUBSYSTEM_POSIX_CUI7O binário usa um subsistema de console POSIX.

Binários do Windows 9x sempre usarão o subsistema Win32, portanto, os únicos valores aceitáveis para estes binários são 2 e 3. Desconheço se binários "nativos" do windows 9x são aceitos.

Offset 01234 56789 ABCDE F
0000 00C0 50 45 00 00 4C010300 A377553C 00000000
0000 00D0 00000000 E0000F01 0B 01 050C 00 02 00 00
0000 00E0 00 0E 00 00 00 00 00 00 00 10 00 00 00100000
0000 00F0 00200000 00 40 00 00 00 10 00 00 00 02 00 00
0000 0100 04000000 00000000 04 00 00 00 00000000
0000 0110 00 50 00 00 00 04 00 00 00000000 02 00 0000

Características de DLL

Este próximo valor de 16 bits indica quando o ponto de entrada deve ser chamado, SE a imagem for de uma DLL. No nosso exemplo, este valor está logicamente zerado (veja acima). Este é mais um campo que parece não ter uso: aparentemente, as DLL recebem notificações de tudo e prescindem deste campo. Novamente os bits são usados para guardar informações:

BitSetado (valor 1)
0Notifica uma anexação de processo (isto é, DLL load)
1Notifica um desligamento de thread (isto é, termina um thread ativo)
2Notifica uma anexação de thread (isto é, cria um thread novo)
3Notifica um desligamento de processo (isto é, DLL unload)

Tamanho da Reserva de Pilha (StackReserve)

Os próximos 4 valores de 32 bits são o tamanho da reserva de pilha ('SizeOfStackReserve'), o tamanho do commit inicial da pilha ('SizeOfStackCommit'), o tamanho da reserva de heap ('SizeOfHeapReserve') e o tamanho do commit do heap ('SizeOfHeapCommit').

As quantidades 'reservadas' são espaços endereçados (não RAM real) que são reservados para um propósito específico. No início do programa, a quantidade "committada" é alocada na RAM. O valor "committado" é também o quanto a pilha ou o heap "committados" irão crescer caso for necessário. Alguns autores alegam que a pilha cresce em páginas, independentemente do valor do 'SizeOfStackCommit'.

Vamos a um exemplo: se o programa possui uma reserva de heap de 1 MB e um commit de heap de 64 Kb, o heap começa com 64 Kb e pode ser expandido até 1 MB. O heap irá crescer de 64 em 64 Kb. O 'heap' neste contexto é o heap primário (default). Um processo pode criar mais heaps se houver necessidade.

Como as DLLs não possuem pilha ou heap próprios, estes valores são ignorados nas suas imagens.

Offset 01234 56789 ABCDE F
0000 00C0 50 45 00 00 4C010300 A377553C 00000000
0000 00D0 00000000 E0000F01 0B 01 050C 00 02 00 00
0000 00E0 00 0E 00 00 00 00 00 00 00 10 00 00 00100000
0000 00F0 00200000 00 40 00 00 00 10 00 00 00 02 00 00
0000 0100 04000000 00000000 04 00 00 00 00000000
0000 0110 00 50 00 00 00 04 00 00 00000000 02 00 0000
0000 0120 00 00 10 00 00 10 00 00 00 00 10 00 00 10 00 00
0000 0130 00000000 10 00 00 00

Loader Flags

Os próximos 32 bits são das 'LoaderFlags' (flags do carregador) para as quais não há uma descrição adequada. No nosso exemplo, de qualquer maneira, todas as flags estão zeradas (veja acima).

Número e Tamanho dos RVA

O número e tamanho dos RVAs ('NumberOfRvaAndSizes') se encontram nos 32 bits seguintes e revelam o número de entradas válidas nos diretórios que vêm logo a seguir. Este número parece não ser muito confiável. No nosso exemplo, veja também acima, são 16 (1000 0000 -> invertendo 0000 0010 -> 16 decimal).

:anota: Exercícios propostos

Para este módulo, tente o seguinte:

  1. Procure por executáveis de todos os tamanhos. Qualquer um serve, contanto que esteja no formato PE. Localize o cabeçalho opcional dos binários e encontre o ponto de entrada, o importantíssimo ENTRY POINT.
  2. Faça alguns exercícios com diversos endereços base de memória e aplique o RVA do ponto de entrada (como explicado no item "Ponto de Entrada do Código do Executável").

Informações adicionais