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) 


Seções

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

Após os cabeçalhos das seções seguem as seções propriamente ditas com os dados nus (raw data). Dentro do arquivo, elas estão alinhadas em 'FileAlignment' bytes, ou seja, após o cabeçalho opcional e após cada uma das seções haverá bytes zerados de preenchimento para atingir o tamanho 'FileAlignment'. Além disto, estão ordenadas pelos seus RVAs. Quando carregadas na RAM, as seções são alinhadas de acordo com o 'SectionAlignment'.

Exemplificando: se o cabeçalho opcional, no arquivo, terminar no offset 981 e o 'FileAlignment' for 512, a primeira seção começará no byte 1024. Note que é possível encontrar o início das seções através do Ponteiro de Dados ('PointerToRawData') ou através do Endereço Virtual ('VirtualAddress') de modo que, dificilmente, será necessário ficar perdendo tempo com alinhamentos.

Existe um cabeçalho de seção para cada seção e cada diretório de dados apontará para uma das seções. Vários diretórios de dados podem apontar para a mesma seção e também podem existir seções que não sejam apontadas pelo diretório de dados.

Os tópicos cobertos neste texto são os seguintes:

  • Considerações gerais
  • Seção 'code'
  • Seção 'data'
  • Seção 'bss'
  • Copyright
  • Símbolos exportados
  • Símbolos importados
  • Recursos
  • Remanejamentos

Considerações gerais

Repetindo: todas as seções estão alinhadas pelo 'SectionAlignment' quando mapeadas na RAM e pelo 'FileAlignment' quando no arquivo em disco (se necessário, reveja em cabeçalho opcional). As seções são descritas através de entradas nos cabeçalhos das seções - encontra-se o início das seções no arquivo em disco através do 'PointerToRawData' e na memória através do 'VirtualAddress'; o tamanho está em 'SizeOfRawData' (se necessário, reveja em cabeçalho das seções).

Existem vários tipos de seções, dependendo do seu conteúdo. Na maioria dos casos (mas não em todos) existe um diretório de dados numa seção, indicado com um ponteiro residente no array do diretório de dados do cabeçalho opcional.

Seção code

Esta seção tem, no mínimo, os bits 'IMAGE_SCN_CNT_CODE', 'IMAGE_SCN_MEM_EXECUTE' e 'IMAGE_SCN_MEM_READ' setados com valor 1 (se necessário, reveja em cabeçalho das seções) e o 'AddressOfEntryPoint' apontará para algum lugar dentro desta seção onde está o início da função que o programador determinou como a primeira a ser executada (se necessário, reveja em cabeçalho opcional).

'BaseOfCode' normalmente apontará para o início desta seção, porém pode apontar além do início se alguns bytes que não são código forem colocados antes do código propriamente dito (se necessário, reveja em cabeçalho opcional).

Geralmente não existe nada além de código executável nesta seção e haverá apenas uma seção com o nome 'code', porém não se fie nisso.

Nomes típicos para esta seção de código executável são ".text", ".code", "AUTO" e coisas do gênero.

O Ponteiro para os Dados ('PointerToRawData') é o offset do início do arquivo em disco até os dados desta seção. No programa exemplo encontramos o valor 0000 0400. Como o tamanho da seção é de 512 bytes (ou 200h), está armazenada em disco de 0000 0400 até o endereço 0000 05FF. Observe os primeiros 16 bytes:

Offset 01234 56789 ABCDE FASCII
0000 0400 6A00E887 010000A3 1C304000 E8770100 j.è‡...£.0@.èw..

Seção data

O próximo assunto será sobre variáveis inicializadas. Esta seção contém variáveis estáticas inicializadas (do tipo "static int i = 5;"). Terá, no mínimo, os bits 'IMAGE_SCN_CNT_INITIALIZED_DATA', 'IMAGE_SCN_MEM_READ' e 'IMAGE_SCN_MEM_WRITE' setados com valor 1 (se necessário, reveja em cabeçalho das seções). Alguns linkers podem colocar constantes em uma seção própria que não possui o bit 'writeable' (escrita permitida). Se parte dos dados for compartilhável ou se existirem outras peculiaridades, pode haver outras seções com os bits de seção apropriadamente setados.

A seção (ou seções) estará situada entre 'BaseOfData' e 'BaseOfData' + 'SizeOfInitializedData'.

Nomes típicos para esta seção são ".data", ".idata", "DATA" e parecidos.

O Ponteiro para os Dados ('PointerToRawData') é o offset do início do arquivo em disco até os dados desta seção. No programa exemplo encontramos o valor 0000 0800. Observe os primeiros 16 bytes:

Offset 01234 56789 ABCDE FASCII
0000 0800 4A616E65 6C614E75 61004A61 6E656C69 JanelaNua.Janeli

Seção bss

O assunto seguinte é sobre dados não inicializados (para variáveis estáticas, tipo "static int k;"). Esta seção é bastante parecida com a seção de dados inicializados, porém terá um offset de arquivo em disco ('PointerToRawData') de 0, indicando que seu conteúdo não está armazenado no arquivo. Além disto, 'IMAGE_SCN_CNT_UNINITIALIZED_DATA' está setado com valor 1 ao invés de 'IMAGE_SCN_CNT_INITIALIZED_DATA', indicando que o conteúdo deve ser preenchido com bytes 0 (zero) durante o carregamento. Isto significa que existe um cabeçalho de seção, porém não existe a seção no arquivo. A seção será criada pelo carregador e consistirá inteiramente de bytes 0. O tamanho será de 'SizeOfUninitializedData'.

Nomes típicos são ".bss", "BSS" e parecidos.

Estas são as seções de dados que NÃO são apontadas através de diretórios de dados. Seu conteúdo e estrutura são fornecidos pelo compilador e não pelo linker.

(O segmento de pilha - stack-segment - e o segmento de heap - heap-segment - não são seções do binário. São criados pelo carregador de acordo com os campos stacksize e heapsize do cabeçalho opcional.)

Como o binário usado como exemplo não possui dados não inicializados, o mesmo não possui uma seção deste tipo.

Copyright

Para começar com uma seção-diretório simples, vamos dar uma olhada no 'IMAGE_DIRECTORY_ENTRY_COPYRIGHT' do diretório de dados. O conteúdo é um copyright ou uma string descritiva em ASCII (não terminada em 0) como "TallyBan control application, copyright © 1800 NhacaSoft & Cia". Normalmente esta string é fornecida ao linker através da linha de comando ou através de um arquivo de descrição.

Esta string não é necessária em tempo de execução e pode ser descartada. Não tem acesso de escrita - na verdade, o aplicativo não precisa de acesso nenhum. Deste modo, o linker vai constatar se já existe ou não uma seção sem acesso de escrita e descartável. Se não existir, cria uma seção com o nome de ".descr" ou algo parecido, joga a string dentro dela e faz com que o ponteiro de diretório de copyright aponte para a string. O bit 'IMAGE_SCN_CNT_INITIALIZED_DATA' também deve ser setado com valor 1.

O programa exemplo não foi compilado com informações de copyright, portanto, o ponteiro de diretório de copyright contém apenas zeros e o binário não apresenta esta seção.

Símbolos exportados

A próxima coisa mais simples é o diretório de exportação, 'IMAGE_DIRECTORY_ENTRY_EXPORT'. Este é um diretório tipicamente encontrado em DLLs. Contém os pontos de entrada de funções exportadas (e os endereços de objetos exportados, etc). É claro que executáveis também podem ter símbolos exportados mas, geralmente, não têm.

A seção que abriga símbolos exportados deve ser do tipo "dados inicializados" e "para leitura". Não deve ser "descartável" porque o processo pode chamar um "GetProcAddress()" para procurar um ponto de entrada de uma função em tempo de execução. Esta seção normalmente é denominada de ".edata" se estiver individualizada; com bastante frequência está fundida com alguma outra seção como "dados inicializados".

A estrutura da tabela de exportação ('IMAGE_EXPORT_DIRECTORY') é composta por um cabeçalho e os pelos dados de exportação, ou seja, o nome dos símbolos, seus ordinais e os offsets dos seus pontos de entrada.

Primeiro temos 32 bits de 'Characteristics' que não são utilizados e normalmente são 0. Depois vem um 'TimeDateStamp' de 32 bits, o qual presumivelmente deveria indicar a data e hora em formato time_t em que a tabela foi criada; aliás, nem sempre é válido (alguns linkers lhe atribuem 0). Depois temos 2 words de 16 bits para a informação da versão ('MajorVersion' e 'MinorVersion') e estes, também, com frequência estão zerados.

Os próximos 32 bits são para o nome ('Name'). Este é um RVA para o nome da DLL representada por uma string ASCII terminada em 0 (o nome é necessário caso o arquivo DLL seja renomeado - veja "binding" no diretório de importação). Depois temos a base ('Base') de 32 bits, que será tratada logo a seguir.

O próximo valor de 32 bits é o número total de itens exportados ('NumberOfFunctions'). Além do seu número ordinal identificador, os itens podem ser exportados sob diversos nomes - e o próximo número de 32 bits é o número total de nomes exportados ('NumberOfNames').

Na maioria dos casos, cada item exportado terá exatamente um nome que lhe corresponde e será usado com este nome, porém um item pode ter vários nomes associados (e ser acessado por cada um deles), ou poderá não ter nome algum e só ser acessível através do seu número ordinal. O uso de exportações sem nome (apenas pelo ordinal) é desencorajado porque todas as versões da DLL exportadora precisariam usar a mesma numeração ordinal gerando um problema de manutenção.

O próximo valor de 32 bits, o 'AddressOfFunctions', é um RVA para a lista dos itens exportados. Aponta para um array de valores de 32 bits de 'NumberOfFunctions', cada qual um RVA para a função exportada ou variável.

Há dois desvios nesta lista. Primeiro, o RVA pode ser 0 e, neste caso, não é utilizado. Segundo, se o RVA apontar para uma seção contendo o diretório de exportação, acaba fazendo uma exportação remetida. Uma exportação prematura é um ponteiro para uma exportação em outro binário - se for usado, a exportação apontada no outro binário é que será usada. Neste caso, o RVA aponta, como mencionado, na a seção do diretório de exportação, para uma string terminada em 0 correspondente ao nome da DLL apontada e o nome exportado separados por um ponto, como "outradll.nomeexportado", ou o nome da DLL e o ordinal exportado, como "outradll.#19".

Chegou a hora de explicar o ordinal de exportação. Este ordinal é um índice para o array AddressOfFunctions, a posição baseada em 0 mais a 'Base' mencionada acima.

Na maioria dos casos a base é 1, o que significa que o primeiro exportado tem um ordinal de 1, o segundo de 2 e assim por diante.

Após o RVA 'AddressOfFunctions' encontra-se um RVA para o array de RVAs de 32 bits para os nomes dos símbolos 'AddressOfNames' e um RVA para o array de ordinais de 16 bits 'AddressOfNameOrdinals'. Ambos possuem elementos 'NumberOfNames'.

Os nomes dos símbolos podem simplesmente não existir. Neste caso, 'AddressOfNames' é 0. Caso contrário, os arrays apontados correm em paralelo, significando que seus elementos em cada item estão atrelados. O array 'AddressOfNames' consiste de RVAs para nomes de exportação terminados em 0; os nomes são mantidos numa lista ordenada (isto é, o primeiro membro do array é o RVA para o nome alfabeticamente menor; isto permite uma procura eficiente quando duma procura de um símbolo exportado pelo nome).

De acordo com a especificação do PE, o array 'AddressOfNameOrdinals' possui um ordinal que corresponde a cada um dos nomes. Entretanto, o que pude verificar é que este array contém o índice atual do array 'AddressOfFunctions'.

Segue um esquema das três tabelas:

'AddressOfFunctions' | V RVA exportado com o ordinal 'Base' RVA exportado com o ordinal 'Base' + 1 ... RVA exportado com o ordinal 'Base' + 'NumberOfFunctions' - 1
'AddressOfNames' 'AddressOfNameOrdinals' | | V V RVA para o primeiro nome <-> Índice da exportação do primeiro nome RVA para o segundo nome <-> Índice da exportação do segundo nome ,,, ... RVA para o nome 'NumberOfNames' <-> Índice da exportação para o nome 'NumberOfNames'

Alguns exemplos para ajudar a entender:

Para achar um símbolo exportado pelo seu ordinal, subtraia a 'Base' para obter o índice, siga o RVA 'AddressOfFunctions' para encontrar o array de exportações e use o índice para achar o RVA exportado no array. Se o RVA não apontar para a seção de exportação, terminou a procura. Caso contrário, ele aponta para uma string que descreve a DLL exportadora e o nome ou o ordinal dentro dela, e você precisa procurar pela exportação remetida adiante.

Para achar um símbolo exportado pelo nome, siga o RVA 'AddressOfNames' (se contiver 0 não existem nomes) para encontrar o array de RVAs para os nomes exportados. Procure o nome desejado na lista. Use o índice do nome no array 'AddressOfNameOrdinals' e obtenha o número de 16 bits correspondente ao nome encontrado. De acordo com a especificação PE, é um ordinal e você precisa subtrair a base para obter o índice de exportação - de acordo com a minha experiência, este já é o índice e você não precisa subtrair a base. Usando o índice de exportação você encontra o RVA de exportação no array 'AddressOfFunctions', sendo ou o próprio RVA de exportação ou um RVA para a string descrevendo uma exportação remetida.

Símbolos importados

Quando o compilador encontra uma chamada para uma função que esteja localizada num executável à parte (geralmente uma DLL), nos casos mais simples ele desconhece as circunstâncias e simplesmente gera uma instrução de chamada normal para este símbolo, cujo endereço o linker terá que ajustar da mesma maneira como faz para qualquer símbolo externo.

O linker usa uma biblioteca de importações para verificar qual é o símbolo de qual DLL que está sendo importado e produz "stubs" (fragmentos) para todos os símbolos importados. Cada fragmento consiste numa instrução de salto e é o alvo da chamada. Estas instruções de salto provocam um salto para um endereço que está na assim chamada tabela de endereços de importação. Em aplicativos mais sofisticados (quando uma "__declspec(dllimporta)" é usada), o compilador conhece a função importada e produz uma chamada direta para o endereço na tabela de endereços de importação sem fazer uso do salto.

De qualquer forma, o endereço da função na DLL sempre é necessário e será suprido pelo carregador a partir do diretório de exportação da DLL exportadora quando o aplicativo for carregado. Verificando o diretório de importação, o carregador sabe quais símbolos de quais bibliotecas precisam ser verificados e ter seus endereços ajustados.

É melhor dar um exemplo. As chamadas com ou sem __declspec(dllimporta) têm o seguinte aspecto:

fonte:

int symbol(char *); __declspec(dllimporta) int symbol2(char*); void foo(void) { int i = symbol("bar"); int j = symbol2("baz"); }

assembly:

... call _symbol ; sem declspec(dllimporta) ... call [__imp__symbol2] ; com declspec(dllimporta) ...

No primeiro caso (sem __declspec(dllimporta)), o compilador não sabia que '_symbol' estava numa DLL de modo que o linker precisa prover a função '_symbol'. Já que a função não está disponível, ele gerará um fragmento de função para o símbolo importado, o qual é um salto indireto. A coleção de todos os fragmentos de importação é denominada de "área de transferência" (às vezes também chamada de "trampolim" porque dá-se um salto para ir para um outro lugar).

Esta área de transferência fica tipicamente localizada na seção de código (code section) mas não faz parte do diretório de importação. Cada um dos fragmentos de função é um salto para a função das DLLs alvo. A área de transferência tem o seguinte aspecto:

_symbol: jmp [__imp__symbol]
_outrosymbol: jmp [__imp__outro__symbol]
... 

Isto significa que, se você utilizar símbolos importados sem especificar "__declspec(dllimporta), então o linker gerará uma área de transferência para eles composta por saltos indiretos. Se você especificar "__declspec(dllimporta)", o compilador fará o redirecionamento e a área de transferência deixa de ser necessária. Isto também significa que, se você importar variáveis ou outra coisa qualquer, é preciso especificar "__declspec(dllimporta)" por que um fragmento com uma instrução de salto é apropriado apenas para funções.

Em qualquer caso, o endereço do símbolo 'x' é armazenado no local '__imp_x'. Todos estes locais juntos compõem a assim chamada "tabela de endereços de importação", a qual é fornecida pelo linker através das bibliotecas de importação das várias DLLs que estejam sendo usadas. A tabela de endereços de importação é uma lista de endereços parecida com:

__imp_symbol: 0xDEADBEEF
__imp_symbol2: 0x40100
__imp_symbol3: 0x300100
... 

Esta tabela de endereços de importação faz parte do diretório de importação e é apontada pelo ponteiro de diretório IMAGE_DIRECTORY_ENTRY_IAT (apesar de que, mesmo que alguns linkers não especifiquem esta entrada de diretório, a coisa também funciona perfeitamente; aparentemente, o carregador consegue resolver importações mesmo sem utilizar o diretório IMAGE_DIRECTORY_ENTRY_IAT).

Os endereços desta tabela são desconhecidos para o linker. O linker insere bobagens (RVAs para os nomes das funções; veja abaixo para maiores informações) que são inseridas pelo carregador em tempo de carregamento usando o diretório de exportação da DLL exportadora. A tabela de endereços de importação e como ela é localizada pelo carregador será descrito em maiores detalhes logo adiante.

Observe que esta descrição é específica para C. Existem outros ambientes de desenvolvimento que não utilizam as bibliotecas de importação. Apesar disso, todos precisam gerar uma tabela de endereços de importação a qual eles usam para permitir que seus programas acessem os objetos e funções importadas. Os compiladores C costumam utilizar as bibliotecas de importação porque é de sua conveniência - seus linkers usam as bibliotecas de qualquer forma. Outros ambientes usam, por exemplo, um arquivo descritivo que lista os nomes das DLLs e os nomes das funções necessárias (como o "arquivo de definição de módulo" - "module definition file") ou uma lista no estilo declaração no fonte.

Esta é a forma como as importações são utilizadas pelo código do programa. Agora vamos dar uma olhada em como é construído um diretório de importação para que o carregador possa usá-lo.

O diretório de importação deve residir numa seção que seja de "dados inicializados" ("initialized data") e "para leitura" ("readable").

O diretório de importação é um array de IMAGE_IMPORT_DESCRIPTORs, um para cada DLL usada. A lista é terminada por um IMAGE_IMPORT_DESCRIPTOR inteiramente preenchido com bytes 0.

O IMAGE_IMPORT_DESCRIPTOR é uma estrutura com os seguintes membros:

  • OriginalFirstThunk: Um RVA (32 bits) apontando para um array terminado em 0 de RVAs para os IMAGE_THUNK_DATAs, cada um descrevendo uma função importada. O array nunca muda.
  • TimeDateStamp: Um carimbo de 32 bits com vários propósitos. Imagine que o timestamp seja 0 e deixe os casos avançados para mais tarde.
  • ForwarderChain: O índice de 32 bits do primeiro remetente (forwarder) na lista de funções importadas. Remetentes também é assunto avançado: para principiantes, setar todos os bits em 1.
  • Name: Um RVA de 32 bits para o nome da DLL (uma string ASCII terminada em 0).
  • FirstThunk: Um RVA de 32 bits para um array de RVAs terminado em 0 para os IMAGE_THUNK_DATAs, cada um descrevendo uma função importada. O array faz parte da tabela de endereços de importação e será mudado.

Percebe-se que cada IMAGE_IMPORT_DESCRIPTOR é um array que fornece o nome da DLL exportadora e, com exceção do remetente (forwarder) e do carimbo (timedatestamp), fornece 2 RVAs para arrays de IMAGE_THUNK_DATAs usando 32 bits. O último elemento de cada array é inteiramente preenchido com bytes 0 para marcar a finalização.

Cada IMAGE_THUNK_DATA é, por enquanto, um RVA para um IMAGE_IMPORT_BY_NAME, o qual descreve a função importada.

O aspecto interessante vem agora: os arrays correm em paralelo, isto é, eles apontam para os mesmos IMAGE_IMPORT_BY_NAMEs.

Não há motivo para se desesperar, vou explicar de outra forma. Este é o conteúdo essencial de um IMAGE_IMPORT_DESCRIPTOR:

OriginalFirstThunk                   FirstThunk
         |                                |
         V                                V
       0 -->          função 1          <-- 0
       1 -->          função 2          <-- 1
       2 -->          função 3          <-- 2
       3 -->            foo             <-- 3
       4 -->       qualquercoisa        <-- 4
       5 --> 0                        0 <-- 5

info O último RVA é 0 (zero)!

Os nomes no centro são os IMAGE_IMPORT_BY_NAMEs. Cada um deles é um número de 16 bits (um hint) seguido por uma quantidade não específica de bytes, sendo o nome em ASCII terminado por 0 do símbolo importado.

O hint é um índice da tabela de nomes da DLL exportadora (veja diretório de exportação acima). O nome neste índice é tentado e, se não houver coincidência, uma busca binária é efetuada para encontrar o nome. Alguns linkers nem se preocupam em procurar hints corretos e simplesmente especificam 1 todas as vezes, ou qualquer outro número arbitrário. Isto não traz problemas, apenas faz com que a primeira tentativa para resolver o nome falhe, forçando desta forma uma busca binária para cada um dos nomes.

Resumindo, se você quiser encontrar informações a respeito da função importada "qualquercoisa" da DLL "nhaca", primeiro precisa encontrar a entrada IMAGE_DIRECTORY_ENTRY_POINT nos diretórios de dados, pegar um RVA, achar este endereço na seção de dados e agora ter à disposição um array de IMAGE_IMPORT_DESCRIPTORs. Pegue o elemento deste array que esteja relacionado com a DLL "nhaca" inspecionando as strings apontadas pelo nome. Quando tiver encontrado o IMAGE_IMPORT_DESCRIPTOR correto, siga seu 'OriginalFirstThunk' e localize o array apontado de IMAGE_THUNK_DATAs. Inspecione os RVAs e encontre a função "qualquercoisa".

Muito bem, mas porque há a necessidade de DUAS listas de ponteiros para os IMAGE_IMPORT_BY_NAMEs? É por que, em tempo de execução, o aplicativo não precisa do nome das funções importadas e sim dos endereços. É onde a tabela de endereços de importação aparece novamente. O carregador irá procurar cada um dos símbolos importados no diretório de exportação da DLL em questão e trocar o elemento do IMAGE_THUNK_DATA da lista de 'FirstThunk' (o qual, até este momento, também apontava para o IMAGE_IMPORT_BY_NAME) pelo endereço linear do ponto de entrada da DLL.

Lembre-se da lista de endereços com labes como "__imp_symbol". A tabela de endereços de importação, apontada pelo IMAGE_DIRECTORY_ENTRY_IAT do diretório de dados, é exatamente a lista apontada pelo 'FirstThunk'. No caso de importações de várias DLLs, a tabela de endereços de importação contém os arrays de 'FirstThunk' de todas elas. A entrada de diretório IMAGE_DIRECTORY_ENTRY_IAT pode estar faltando e, mesmo assim, as importações funcionarão bem.

O 'OriginalFirstThunk' permanece intocado, de modo que sempre é possível analisar a lista original de nomes importados.

A importação, agora, foi adequada com os endereços lineares corretos e tem o seguinte aspecto:

OriginalFirstThunk                   FirstThunk
         |                                |
         V                                V
       0 -->          função 1          <-- função 1 exportada
       1 -->          função 2          <-- função 2 exportada
       2 -->          função 3          <-- função 3 exportada
       3 -->            foo             <-- foo exportada
       4 -->       qualquercoisa        <-- qualquercoisa exportada
       5 --> 0                        0 <-- 5

Esta é a estrutura básica para casos simples. Agora veremos uns macetes dos diretórios de importação.

Primeiramente, o bit IMAGE_ORDINAL_FLAG no array (ou seja, o MSB) do IMAGE_THUNK_DATA pode ser setado (valor 1). Neste caso, não existem informações de nomes de símbolos na lista e o símbolo é importado exclusivamente através do seu ordinal. Você obtém o ordinal inspecionando o word menos significante (lower word) do IMAGE_THUNK_DATA.

A importação através de ordinais é desencorajada. É muito mais seguro importar por nomes porque os ordinais de exportação podem mudar se a DLL exportadora não for a da versão esperada.

Em segundo lugar, existem as assim chamadas "importações casadas" (bound imports).

Imagine as tarefas do carregador: quando um binário que ele queira executar precisa de uma função de uma DLL, o carregador carrega a DLL, localiza seu diretório de exportação, encontra o RVA da função e calcula o ponto de entrada da função. A seguir, substitui na posição correspondete da lista 'FirstThunk' o endereço assim encontrado.

Supondo que o programador tenha sido esperto e forneceu um endereços preferenciais de carregamento únicos para as DLLs, que não colidam com nada, pode-se inferir que os pontos de entrada das funções serão sempre os mesmos. Eles podem ser calculados e substituir a lista de 'FirstThunk' em tempo de linkagem e é exatamente isto o que ocorre com as "importações casadas". (O utilitário "bind" faz isto; faz parte do Win32 SDK.)

É claro que todo cuidado é pouco: a DLL do usuário pode ser de uma versão diferente ou talvez seja necessário remanejar a DLL e, desta forma, a lista de 'FirstThunk' substituída torna-se inválida. Neste caso, o carregar ainda poderá optar pela lista de 'OriginalFirstThunk', achar os símbolos importados e fazer a substituição na lista de 'FirstThunk' novamente. O carregador sabe que isto é necessário quando: a)a versão da DLL exportadora não confere ou b) a DLL exportadora foi remanejada.

Não é problema para o carregador constatar que houve remanejamentos mas, e no caso de versões diferentes? É onde o 'TimeDateStamp' do IMAGE_IMPORT_DESCRIPTOR entra em ação. Se for 0, a lista de importação não foi ligada e o carregador precisa corrigir os pontos de entrada. Caso contrário, as importações estão casadas e o 'TimeDateStamp' precisa ser igual ao 'TimeDateStamp' do cabeçalho de arquivo da DLL exportadora. Se não conferir, o carregador assume que o binário está casado com a DLL "errada" e (fará o divórcio ? hehehe, não...) fará uma nova substituição na lista de importação.

Existe um macete adicional a respeito dos "remetentes" (forwarders) na lista de importação. A DLL pode exportar um símbolo que não esteja definido na prórpia DLL, mas sim, importado de outra DLL. Tal símbolo é denominado de remetido (forwarded).

Obviamente, não é possível constatar se o ponto de entrada deste símbolo é válido apenas olhando para o timestamp de uma DLL não contém o dito ponto de entrada. Por uma questão de segurança, os pontos de entrada de símbolos remetidos precisam ser corrigidos. Na lista de importação de um binário, as importações de símbolos remetidos precisam ser localizados para que o carregador possa fazer as substituições necessárias.

Isto é feito através da cadeia de remessa ('ForwarderChain'). É um índice dentro da lista de thunk. A importação na posição indexada é uma exportação remetida e o conteúdo da lista de 'FirstThunk' nesta posição é o índice da PRÓXIMA importação remetida, e assim por diante, até que o índice seja "-1", indicando que não existem mais remessas. Se não existirem remetidos, a 'ForwarderChain' é -1.

O que foi dito até agora constitui o assim chamado "estilo antigo" de ligações. Neste ponto seria interessante rever a matéria wink

Muito bem. Vou assumir que você tenha achado o IMAGE_DIRECTORY_ENTRY_IMPORT e que, seguindo-o, tenha encontrado o diretório de importações, o qual estará contido em uma das seções. Agora você se encontra no início de um array de IMAGE_IMPORT_DESCRIPTORs, o último dos quais está totalmente preenchido com 0.

Para decifrar um dos IMAGE_IMPORT_DESCRIPTORs, você olha primeiro o campo 'Name', segue o RVA e efetivamente encontra o nome da DLL exportadora. A seguir, você determina se as importações estão casadas ou não (solteiras???) - o 'TimeDateStamp' diferente de 0 indica importações casadas. Se estiverem casadas, agora é uma boa hora para checar se a versão da DLL confere com a sua comparando o 'TimeDateStamp' de ambas.

Agora você segue o RVA do 'OriginalFirstThunk' para chegar no array IMAGE_THUNK_DATA. Percorra este array (o último elemento está preenchido com 0) e cada um dos elementos será um RVA de um IMAGE_IMPORT_BY_NAME (a não ser que o bit mais significante esteja setado, fazendo com que você não tenha um nome mas um mero ordinal à disposição). Siga o RVA e pule dois bytes (o hint). Agora você está frente a frente com uma string ASCII terminada em 0 que é o nome da função importada.

Para achar o endereço do ponto de entrada fornecido no caso de ser uma importação casada, siga o 'FirstThunk' e desloque-se paralelamente ao array 'OriginalFirstThunk'. Os elementos do array são os endereços lineares do pontos de entrada (esquecendo o tópico dos remetentes por um momento).

Há uma coisa que não foi mencionada até agora: aparentemente existem linkers que geram um bug quando constroem o diretório de importação (encontrei este bug usando um linker do Borland C). Estes linkers atribuem o valor 0 ao 'OriginalFirstThunk' no IMAGE_IMPORT_DESCRIPTOR e criam apenas o array de 'FirstThunk'. É óbvio que diretórios deste tipo não podem ser ligados (as informações necessárias para reconstruir as importações foram perdidas - você não vai conseguir achar os nomes das funções). Neste caso, você terá que seguir o array do 'FirstThunk' para obter os nomes dos símbolos importados e você nunca terá endereços de pontos de entrada pre-ajustados. Encontrei um documento do TIS ([6]) descrevendo o diretório de importação de uma maneira que é compatível com este bug. Esta, provavelmente, é a origem deste bug. O documento da TIS especifica:

IMPORT FLAGS
TIME/DATE STAMP
MAJOR VERSION - MINOR VERSION
NAME RVA
IMPORT LOOKUP TABLE RVA
IMPORT ADDRESS TABLE RVA 

contrariamente à estrutura proposta em outras especificações:

OriginalFirstThunk
TimeDateStamp
ForwarderChain
Name
FirstThunk

O último macete sobre diretórios de importação é a assim chamada ligação no "estilo novo" (new style binding) (descrito em [3]), que também pode ser obtida com o utilitário "bind". Quando usado, todos os bits do 'TimeDateStamp' são setados em 1 e não existe a ForwarderChain. Todos os símbolos importados têm seus endereços substituídos, sendo do tipo remetido ou não. Mesmo assim, ainda é preciso conhecer a versão das DLLs e distinguir símbolos remetidos dos normais. Para este propósito, o diretório IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT é criado. Este diretório, se é que entendi bem, NÃO ficará numa seção, mas sim no cabeçalho, logo após os cabeçalhos das seções a antes da primeira seção. (Não fui eu quem invetou esse negócio, estou apenas descrevendo!)

Este diretório indica, para cada uma das DLLs usadas, de quais outras DLLs vem as exportações remetidas.

A estrutura é um IMAGE_BOUND_IMPORT_DESCRIPTOR contendo (nesta ordem):

  • Um número de 32 bits com o 'TimeDateStamp' da DLL
  • Um número de 16 bits, 'OffsetModuleName', que é o offset do início do diretório para o nome terminado em 0 da DLL
  • Um número de 16 bits, 'NumberOfModuleForwarderRefs', que é o número de DLLs que essa DLL usa para seus remetentes.

Imediatamente após esta estrutura encontra-se a estrutura do 'NumberOfModuleForwarderRefs' que contém os nomes e versões das DLLs que a partir das quais esta DLL faz as remessas. Estas estruturas são 'IMAGE_BOUND_FORWARDER_REF:

  • Um número de 32 bits para o 'TimeDateStamp'
  • Um número de 16 bits para o 'OffsetModuleName', que é o offset do início do diretório para o nome terminado em 0 da DLL remetente.
  • 16 bits não utilizados

Seguindo o 'IMAGE_BOUND_FORWARDER_REF' fica o próximo 'IMAGE_BOUND_IMPORT_DESCRIPTOR' e assim sucessivamente. A lista é terminada por um IMAGE_BOUND_IMPORT_DESCRIPTOR com todos os bits zerados.

É isso aí, este é o jeitão do "novo estilo".

Agora, se você tem um diretório de importação no "novo estilo", você carrega todas as DLLs, usa o ponteiro de diretório IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT para achar o IMAGE_BOUND_IMPORT_DESCRIPTOR, rastreia o mesmo e checa se os 'TimeDateStamp' das DLLs carregadas conferem com as indicadas neste diretório. Se não, acerte-as no array do 'FirstThunk'do diretório de importação.

Recursos

Os recursos (resources) como caixas de diálogo, menus, ícones e assim por diante são armazenados no diretório de dados apontado pelo IMAGE_DIRECTORY_ENTRY_RESOURCE. Está numa seção que tem pelo menos os bits 'IMAGE_SCN_CNT_INITIALIZED_DATA' e 'IMAGE_SCN_MEM_READ' setados.

Uma base de recursos é o 'IMAGE_RESOURCE_DIRECTORY' que contém várias 'IMAGE_RESOURCE_DIRECTORY_ENTRY', cada uma das quais, por sua vez, pode apontar para um 'IMAGE_RESOURCE_DIRECTORY'. Desta forma obtém-se uma árvore de 'IMAGE_RESOURCE_DIRECTORY' com 'IMAGE_RESOURCE_DIRECTORY-ENTRY' como folhas. Estas folhas apontam para os dados de recursos propriamente ditos.

Na vida real a situação é um tanto tranquila. Você normalmente não vai encontrar árvores complexas, difíceis de serem analisadas.

A hierarquia geralmente é a seguinte: um diretório é o raiz. Ele aponta para diretórios, um para cada tipo de recurso. Estes últimos apontam para subdiretórios, cada um deles com um nome ou ID, e que apontam para um diretório das línguas disponíveis para este recurso. Para cada língua há uma entrada de recurso, a qual, finalmente, vai apontar os dados. (Observe que recursos multi-língua não funcionam no Win95, que sempre usa o mesmo recurso se estiver disponível em várias línguas - Não testei qual, mas imagino que seja o primeiro que ele encontrar. Funciona no NT.)

A árvore, sem o ponteiro para os dados, tem o seguinte aspecto:

                           raiz
      -----------------------------------------------
      |                     |                       |
     menu                dialog                    icon
   ---------          ------------         -------------------
   |       |          |          |         |        |        |
 main     popup      0x10     maindlg    0x100    0x110    0x120
   |       |          |          |         |        |        |
english  português  default  português  default  default  default

Um IMAGE_RESOURCE_DIRECTORY compõe-se de:

  • 32 bits de flags não utilizadas denominadas 'Characteristics'
  • 32 bits para 'TimeDateStamp' (novamente na representação time_t) que indica a data e hora da criação do recurso (se a entrada estiver setada)
  • 16 bits para 'MajorVersion' e 16 bits para 'MinorVersion', permitindo a manutenção de várias versões do recurso
  • 16 bits para 'NumberOfNamedEntries' e outros 16 bits para 'NumberOfIdEntries'

Logo após esta estrutura ficam as estruturas 'NumberOfNamedEntries' + 'NumberOfIdEntries' que estão no formato 'IMAGE_RESOURCE_DIRECTORY_ENTRY', as nominadas constando primeiro. Podem apontar para outros 'IMAGE_RESOURCE_DIRECTORY' ou apontam diretamente para os dados de recurso.

Uma 'IMAGE_RESOURCE_DIRECTORY_ENTRY' consiste de:

  • 32 bits com a id do recurso ou o diretório que o descreve
  • 32 bits com o offset para os dados ou o offset para o próximo subdiretório

O significado da id depende do nível na árvore. A id pode ser um número (se o bit mais significante for zero) ou um nome (se o bit mais significante estiver setado). Se for um nome, os 31 bits restantes são o offset do início dos dados da seção de recursos até o nome (o nome tem o comprimento de 16 bits e trailing wide characters, em unicode, não terminado em 0).

Se você estiver no diretório raiz, a id, se for um número, indica o tipo de recurso:

  1. cursor
  2. bitmap
  3. ícone
  4. menu
  5. diálogo
  6. tabela de strings
  7. diretório de fontes
  8. fonte
  9. aceleradores
  10. dados não formatados de recursos
  11. tabela de mensagens
  12. grupo de cursor
  13. grupo de ícones
  14. informação da versão

Qualquer outro número é definido pelo usuário. Qualquer tipo de recurso (resourece-type) com um tipo de nome (type-name) é sempre definido pelo usuário.

Se você estiver um nível abaixo, a id é a id do recurso (ou nome do recurso).

Se você estiver mais um nível abaixo, a id precisa ser um número e é a id de língua da instância específica do recurso. Por exemplo, pode haver o mesmo diálogo em inglês australiano, francês canadense e alemão suiço, todos eles dividem a mesma id de recurso. O sistema irá escolher o diálogo que deve ser carregado baseado no locale do thread, o qual, por sua vez, usualmente reflete a "configuração regional" do usuário. (Se o recurso não puder ser encontrado para o locale thread, o sistema primeiro tentará achar um recurso para o locale usando uma sublíngua neutra, por exemplo, vai procurar o francês padrão ao invés do francês canadense do usuário; se ainda assim não consegue achar, a instância com a menor id de língua será usada. Tudo isto funciona apenas no NT).

Para decifrar a id da língua, divida-a na id da língua primária e na id da sublíngua usando as macros PRIMARYLANGID() e SUBLANGID() que fornecerão bits de 0 a 9 e de 10 a 15, respectivamente. Os valores estão definidos no arquivo "winresrc.h".

Recursos de língua só são apoiados para aceleradores, diálogos, menus, rcdata ou tabelas de strings (stringtables). Outros tipos de recursos deverão ser LANG_NEUTRAL/SUBLANG_NEUTRAL.

Para descobrir se o nível abaixo de um diretório de recursos é outro diretório, inspecione o bit mais significativo do offset. Se estiver setado (valor 1), os 31 bits restantes são o offset do início dos dados da seção de recursos até o próximo diretório, novamente no formato IMAGE_RESOURCE_DIRECTORY com entradas IMAGE_RESOURCE_ENTRYs.

Se o bit estiver re-setado (valor 0), o offset é a distância entre o início dos dados da seção de recursos e a descrição dos dados, uma IMAGE_RESOURCE_DATA_ENTRY. Consiste de 32 bits 'OffsetToData' (o offset para os dados contando do início dos dados da seção de recursos), 32 bits para o tamanho ('Size' ) dos dados, 32 bits 'Code Page' e 32 bits não utilizados.

(O uso de codepages não é recomendado. Deve-se dar preferência para 'language' - maneira mais adequada para apoiar múltiplos locales.)

O formato dos dados depende do tipo de recurso. Descrições podem ser encontradas na documentação do MS SDK. Note que qualquer string de recurso está sempre em UNICODE, exceto nos recursos definidos pelo usuário, onde estão no formato escolhido pelo usuário.

Remanejamentos

O último diretório de dados que será descrito é o diretório da base de remanejamento (base relocation directory). Ele é apontado por uma entrada IMAGE_DIRECTORY_ENTRY_BASERELOC nos diretórios de dados do cabeçalho opcional. Tipicamente fica contida numa seção própria, com um nome do tipo ".reloc" e os bits IMAGE_SCN_CNT_INITIALIZED_DATA, IMAGE_SCN_MEM_DISCARDABLE e IMAGE_SCN_MEM_READ setados.

Os dados de remanejamento são necessários para o carregador caso a imagem não possa ser mapeada para o endereço de carregamento preferencial 'ImageBase' mencionado no cabeçalho opcional. Neste caso, os endereços fixos fornecidos pelo linker deixam de ser válidos e o carregador precisa fazer correções nos endereços absolutos usados para a localização de variáveis estáticas, string literais e assim por diante.

O diretório de remanejamentos é uma sequência de blocos (chunks). Cada bloco contém a informação de remanejamento para 4 Kb da imagem. Todo bloco começa com uma estrutura 'IMAGE_BASE_RELOCATION'. Esta estrutura consiste em 32 bits para a 'VirtualAddress' e 32 bits para o 'SizeOfBlock', seguidos pelos dados de remanejamento, cada um com 16 bits.

O 'VirtualAddress' é o RVA base que precisa ser aplicado no remanejamento deste bloco. O 'SizeOfBlock' é o tamanho do bloco inteiro em bytes.

O número de remanejamentos de preenchimento é ('SizeOfBlock' - sizeof(IMAGE_BASE_RELOCATION)) / 2. A informação de remanejamento termina com uma estrutura IMAGE_BASE_RELOCATION com um 'VirtualAddress' de 0.

Cada 16 bits de informação de remanejamento consiste da posição de remanejamento nos 12 bits menos significativos e o tipo de remanejamento nos 4 bits mais significativos. Para obter o RVA do remanejamento precisa-se adicionar à 'VirtualAddress' da IMAGE_BASE_RELOCATION os 12 bits de posição. O tipo é um dos seguintes:

  • IMAGE_REL_BASED_ABSOLUTE (0): Isto é um no-op (não operacional). É usado para alinhar o bloco numa borda de 32 bits. A posição deve ser 0.
  • IMAGE_REL_BASED_HIGH (1): Os 16 bits mais significativos do remanejamento precisam ser aplicados aos 16 bits do WORD apontado pelo offset, o qual é o word mais significativo de um DWORD de 32 bits.
  • IMAGE_REL_BASED_LOW (2): Os 16 bits menos significativos do remanejamento precisam ser aplicados aos 16 bits do WORD apontado pelo offset, o qual é o word menos significativo de um DWORD de 32 bits.
  • IMAGE_REL_BASED_HIGHLOW (3): Todos os 32 bits do remanejamento precisam ser aplicados aos 32 bits em questão. Isto (e o no-op '0') é o único remanejamento que encontrei até agora em binários.
  • IMAGE_REL_BASED_HIGHADJ (4): Este é só para os corajosos. Leia você mesmo (na referência bibliográfica [6]) e veja se consegue entender:
    "Highadjust. Esta correção requer um valor completo de 32 bits. Os 16 bits mais significativos estão localizados em Offset e os 16 bits menos significativos estão localizados no próximo elemento do array Offset (este elemento de array está incluído on campo Size). Ambos precisam ser combinados numa variável com sinal. Adicione o delta de 32 bits. Depois adicione 0x8000 e armazene os 16 bits mais significativos da variável com sinal no campo de 16 bits em Offset."
  • IMAGE_REL_BASED_MIPS_JMPADDR (5): Desconhecido
  • IMAGE_REL_BASED_SECTION (6): Desconhecido
  • IMAGE_REL_BASED_REL32 (7): Desconhecido

Como um exemplo, se você verificar que as informações de remanejamento são

0x00004000     32 bits     RVA inicial
0x00000010     32 bits     tamanho do bloco
0x3012         16 bits     dados para remanejamento
0x3080         16 bits     dados para remanejamento
0x30F6         16 bits     dados para remanejamento
0x0000         16 bits     dados para remanejamento
0x00000000                 (RVA do próximo bloco)
0xFF341234	 	 

você sabe que o primeiro bloco descreve remanejamentos iniciando no RVA 0x4000 e tem o comprimento de 16 bytes. Como o cabeçalho usa 8 bytes e um remanejamento usa 2 bytes, há (16 - 8) / 2 = 4 remanejamentos no bloco.

O primeiro remanejamento deve ser aplicado no DWORD em 0x4012, o próximo no DWORD em 0x4080 e o terceiro no DWORD em 0x40F6. O último remanejamento é no-op.

O próximo bloco tem um RVA de 0 e termina a lista.

Agora, como é que se faz um remanejamento?

Sabe-se que a imagem é remanejada para o endereço preferencial de mapeamento 'ImageBase' do cabeçalho opcional. Também se conhece o endereço para onde a imagem foi mapeada. Se ambos coincidem, nada precisa ser feito. Se não coincidem, calcula-se a diferença base_atual - base_preferencial e adiciona-se este valor (com sinal, pois pode ser negativo) às posições de remanejamento, as quais são localizadas de acordo com o método descrito acima.

:anota: Exercícios propostos

Para este módulo, tente o seguinte:

  1. Identifique as DLLs usadas no programa exemplo
  2. Identifique as funções que são importadas das DLLs

Informações adicionais