8.3 Chamadas de procedimento
8.3.1 Conceitos
O ato de descobrir o endereço de um procedimento (ou variável global) é chamado binding5, e o mecanismo de atrasar o binding até ele ser necessário é chamado de lazy binding.
Lazy6é um termo genérico para uma descrever uma família de otimizações onde o trabalho a ser feito é atrasado até o momento onde ele é efetivamente necessário. Exemplos de mecanismos “preguiçosos” incluem copy-on-write[7] (utilizado prin- cipalmente em sistemas operacionais) e lazy evaluation[18] (utilizado em linguagens de programação).
O ELF apresenta um mecanismo muito criativo e eficiente para implementar chamadas de procedimento cujo alvo encontra- se em bibliotecas compartilhadas e que utiliza lazy binding.
Quando alguém diz que algo é “criativo e eficiente”, o leitor pode ver nas entrelinhas que isto significa “complexo”. E isto ocorre aqui, e com um considerável grau de complexidade.
Para permitir a explicação, esta seção apresenta conceitos de hardware e organização em tempo de execução imprescindíveis para a descrição do funcionamento do mecanismo de chamadas de procedimento em bibliotecas compartilhadas: a seção 8.3.1.1 apresenta instruções de desvio do AMD64 cujo parâmetro não é o endereço do procedimento-alvo e a seção 8.3.1.2 apresenta a seção.pltcriada para permitir a combinação da instruçãocallcom a instrução de desvio indireto. Por fim, a seção 8.3 descreve a seção.got.plt, que contém os alvos dos procedimentos das bibliotecas compartilhadas.
8.3.1.1
Instruções de desvio
As instruções de desvio utilizadas neste livro até o momento utilizam um parâmetro que é o endereço para onde desviar. É o caso de"jmp 0x601030"e"call 0x601030". Porém, quando o endereço do alvo não é constante, como é o caso das bibliotecas compartilhadas, outras abordagens são necessárias.
5 neste contexto, a melhor tradução é “vínculo”, mas adotaremos o termo em inglês 6 “preguiçoso”
O AMD64 inclui outras categorias de instrução de desvio chamadas “instruções de desvio relativo ao PC” e “instruções de desvio indireto” que são utilizadas na implementação do mecanismo de desvio para procedimentos em bibliotecas compartilha- das.
A primeira categoria de instruções de desvio são conhecidas como relativas ao PC7[12]. Nestas instruções, o alvo é uma cons- tante que será somada ao%rip. Esta categoria se assemelha às instruções de endereçamento indireto vistas na GOT (seção 8.2.1) porém sem o(%rip), que fica subentendido.
A instrução abaixo exemplifica as instruções de desvio relativas ao PC no AMD64:
call 0xFFFFFE1E
Ao ser executada, esta instrução desvia o fluxo para um endereço0xFFFFFE1Ebytes distante do%rip, mas como é usado complemento de dois, este número pode ser positivo ou negativo. Neste caso,0xFFFFFE1E16 = −48210, ou seja, o fluxo é desvi- ado para um endereço anterior ao atual.
Instrução de desvio relativo ao PC funcionalidade
call 0xFFFFFE1E %rip ← %rip + 0xFFFFFE1E
O AMD64 apresenta variações desta instrução que não abordaremos aqui, mas que podem ser encontradas em [37]. Cada uma delas tem um código de operação diferente, e a que nós usaremos é a variação que indica que o parâmetro é uma constante de 32 bits com sinal (que permite alvos distantes até 2G, de −231até 231−1 , bytes distantes de%rip). Nesta variação, o código de operação éE8, e a instrução acima seria visualizada nas ferramentas convencionais (objdump,readelfegdb). Estas ferramentas mostram o código hexadecimal da instrução é:
call 0xFFFFFE1E⇔ E8 1E FE FF FF
Aliás, a única diferença entre as várias categorias decallé o código de operação, o que dificulta saber qual é a utilizada olhando só o código assembly (todas são mapeadas paracall).
Esta primeira categoria de instruções é uma das ferramentas utilizadas para gerar código independente de localização, uma vez que o alvo é relativo e não absoluto. Isto permite colocar uma biblioteca em qualquer posição de memória sem necessidade de relocação para as chamadas que a biblioteca faz aos procedimentos da mesma biblioteca.
Agora, vamos apresentar a segunda categoria de instruções de desvio, que são utilizadas na implementação do mecanismo de desvio para procedimentos em bibliotecas compartilhadas, as “instruções de desvio indireto”[12].
Nas instruções de desvio convencionais o parâmetro indica o endereço-alvo do desvio. Assim, a funcionalidade da instrução
jmp 0x601030pode ser visto como %rip←0x601030.
Já nas instruções de desvio indireto (identificadas pelo uso de um asterisco à frente do parâmetro) o parâmetro indica o endereço que contém o alvo do desvio.
A funcionalidade da uma instrução de desvio indireto é apresentada abaixo, onde o alvo do desvio é o valor contido na posição de memória indicada pelo parâmetro.
Instrução de desvio indireto funcionalidade
jmp *0x601030 %rip ← M[0x601030]
8.3.1.2
A seção .plt
O ELF combina as duas instruções de desvio apresentadas na seção 8.3.1.1 para implementar o mecanismo de chamadas a pro- cedimentos em bibliotecas compartilhadas da seguinte forma:
• o formato ELF reserva uma seção que se faz presente tanto no programa principal quanto em cada uma das bibliotecas. Em tempo de execução, cada procedimento contido nesta seção funciona como um “trampolim” para cada procedimento em bibliotecas compartilhadas. Esta seção é chamada.plt8, e como ela fica próxima à seção.text, é comum utilizar a instrução call relativo para acessar os seus procedimentos;
• dentro da seção.plthá uma entrada para cada procedimento remoto chamado. Por exemplo, se o procedimento remoto forproc, haverá uma entrada na plt com o nomeproc@plt. O procedimentoprocnunca é chamado diretamente (ou seja, nunca écall proc), mas simcall proc@plt, que é quem empilha o endereço de retorno.
7 PC-relative
8.3. Chamadas de procedimento 141
• cada entrada da plt faz um desvio indireto para um endereço contido numa região específica da memória chamada
.got.pltque contém o endereço-alvo do procedimento.
• após a execução do procedimento, o fluxo retornará ao endereço de retorno especificado pela instruçãocall proc@plt. A figura 30 apresenta um modelo esquemático de chamadas a procedimentos em bibliotecas compartilhadas. A figura mostra que foram carregadas duas bibliotecas compartilhadas. A superior (libMySharedLib.so) contém a implementação das funções
a()eb()enquanto que a inferior (libc.so.6) indica a implementação da funçãoprintf()9
..
.
Segmentos de Processo .text .data .got .got.plt Segmento Dinâmico a(){. . . } b(){. . . } printf(){. . . } Text Text .got.plt[0] .got.plt[1] .got.plt[2] .got.plt[3] .got.plt[4] . . .•
•
•
Figura 30 – Biblioteca compartilhada: desvio indireto aos procedimentos do segmento dinâmico
(usando a.got.plt
A seção.plté composta por n + 1 entradas cujos rótulos seguem o padrão<nome-do-proced>@plt, sendo que há uma entrada por procedimento chamado e uma entrada especial que chamaremos deplt0.
1
<procn@plt>:
2jmp *got.plt[n]
3
pushq n
4
jmp plt0
Algoritmo 73: Procedimento genérico da seção.plt.
Cada um dos procedimento na seção.pltocupa exatamente 16 bytes. e tem um código muito semelhante ao apresentado no algoritmo 73:
linha 1 rótulo que indica a n-ésima entrada na seção plt. Este rótulo segue o padrão<nome-do-proced>@plt.
9 A bibliotecalibc.so.6ocupa em torno de 1.8M, e contém a implementação de muito mais procedimentos. Confira com
linha 2 desvia indireto para o alvo indicado emgot.plt[n].
linha 3 empilha o número n, que indica qual entrada nagot.plt[n]deve ser atualizado.
linha 4 desvia o fluxo para a 0-ésima entrada na seção.plt.
O código está dividido em duas partes. As linhas 1 e 2 são executadas em todas as chamadas ao procedimento. Já as linhas 3 e 4 são executadas uma única vez, na primeira chamada.
Vamos analisar agora o que ocorre na primeira vez que<procn@plt>é chamado. O objetivo desta primeira chamada é colocar emgot.plt[n]o endereço do procedimentoprocque será utilizada a partir da segunda chamada.
Inicialmente,got.plt[n]contém o endereço da instrução da linha 3 (pushq n). É muito esquisito, mas é isso mesmo: na primeira chamada,jmp *got.plt[n]desvia indiretamente o fluxo para a instrução seguinte:pushq n.
Esta instrução empilhan, indicando qual entrada dagot.pltdeve ser alterada para em seguida desviar o fluxo parajmp plt0, que é a primeira entrada da seçãoplt. Em linhas gerais, ela usa o parâmetro empilhado (n), empilha o endereço de uma estrutura de dados indicada emgot.plt[0]e desvia o fluxo para o procedimento cujo endereço está contido emgot.plt[1].
O procedimento apontado porgot.plt[1]é também conhecido como “resolver”10. Este procedimento recebe dois parâ- metos empilhados (negot.plt[0]) para ajustar o endereço-alvo emgot.plt[n]e, ao final, desviar o fluxo para o procedimento- alvo.
Observe como termo o lazy binding se aplica neste caso: somente na primeira chamada é que o endereço do procedimento- alvo será efetivamente atualizado. Se não for chamado nenhuma vez, então não há necessidade de atualizar a sua entrada na
got.plt.
O que foi visto aqui é o proposto noELFe cada arquitetura implementa de acordo com as ferramentas disponíveis. Por exemplo, como fazê-lo em uma arquitetura sem instruções de desvio indireto ou sem desvio relativo ao PC? (veja exercício 8.1).
A seção 8.3.2 descreve o funcionamento deste mecanismo de lazy binding doELFaplicado aoAMD64.