ARQUITECTURA DE COMPUTADORES CAPÍTULO II AULA X Ricardo Mendão Silva [email protected] Índice • Traduzindo e iniciando uma aplicação • • • • • • Compiladores Assembladores Linkers Loaders DLLs Iniciando um programa em Java Ricardo Mendão Silva [email protected] Traduzindo uma aplicação • Após toda a matéria abordada nesta disciplina, chega o momento de analisarmos ao detalhe como passamos, de facto, de um excerto de código presente num ficheiro local, para uma aplicação em execução. Ricardo Mendão Silva [email protected] Traduzindo uma aplicação • Numa abordagem de alto nível: 1. O programa é compilado para linguagem assembly e assemblado num objecto (módulo) em linguagem máquina (010101010…). 2. O linker combina vários módulos com bibliotecas (de rotinas) para resolver todas as referências, 3. O loader colocar o código máquina nos devidos locais de memória para serem executados pelo processador. • Para acelerar o processo alguns compiladores directamente os módulos, enquanto que noutros casos são utilizados linking-loaders, que executam as duas últimas acções numa só. Ricardo Mendão Silva [email protected] Traduzindo uma aplicação • Para identificar os diferentes ficheiros, o UNIX segue a seguinte convenção: UNIX MS-DOS Ficheiros de código C .c .C Ficheiros de assembly .s .ASM Objectos compilados .o .OBJ bibliotecas estáticas .a .LIB bibliotecas dinâmicas .so .DLL Executáveis a.out .EXE Ricardo Mendão Silva [email protected] Índice • Traduzindo e iniciando uma aplicação • • • • • • Compiladores Assembladores Linkers Loaders DLLs Iniciando um programa em Java Ricardo Mendão Silva [email protected] Traduzindo uma aplicação Compilador C • O compilador transforma os programas em C em programas em linguagem assembley. • Como já sabemos, linguagem assembley acaba por ser uma forma simbólica entendida pela máquina. • Como também já sabemos, linguagens de programação de alto nível requerem muito menos linhas de código do que a linguagem assembley, o que permite aumentar bastante a produtividade. • Em 1975 diversos sistemas operativos e assembladores foram escritos em linguagem assembly porque as memórias eram pequenas e os compiladores ineficientes. • Actualmente as memórias avançaram bastante, tanto em capacidade, velocidade como em preço, e os compiladores tornaram-se tão eficientes quanto qualquer programador profissional de assembley. Ricardo Mendão Silva [email protected] Índice • Traduzindo e iniciando uma aplicação • • • • • • Compiladores Assembladores Linkers Loaders DLLs Iniciando um programa em Java Ricardo Mendão Silva [email protected] Traduzindo uma aplicação Assemblador • Uma vez que a linguagem assembly é a interface entre o hardware e as linguagens de alto nível, o assemblador consegue tratar variações de linguagem máquina, como se de instruções reais se tratassem – pseudo-instruções. • move $t0,$t1 add $t0,$zero,$t1 • Deste modo o assemblador acaba por nos fornecer mais instruções do que aquelas suportadas pelo hardware, facilitando assim a programação. • Para além disso, os assembladores ainda aceitam uma série de variantes numéricas, não só binário ou decimal, mas também hexadecimal. Ricardo Mendão Silva [email protected] Traduzindo uma aplicação Assemblador • Apesar das “facilidades” apresentadas no slide anterior, a verdadeiras função do assemblador é mesmo converter as instruções em código máquina. • O assemblador, ao assembler assembley, cria um objecto (um ficheiro) que contem não só o código máquina correspondente, mas também os dados e a informação necessária para colocar as instruções em memória. • Para criar a versão binária (código máquina) o assemblador necessita de determinar todos os endereços referentes às labels existentes no código, branches e instruções de transferência de dados. • Para tal essa informação é guardada numa tabela de símbolos. Ricardo Mendão Silva [email protected] Traduzindo uma aplicação Assemblador Estrutura Object file em UNIX Cabeçalho (object file header) Descreve o tamanho e posição de outras peças do objecto. Segmento de texto Contem o código em linguagem máquina. Segmento de dados estáticos Contem os dados alocados para toda a “vida” do programa. Em UNIX é possível utilizar alocação dinâmica. Informação de realocação Identifica instruções e dados que dependem de endereços absolutos quando o programa é carregado em memória. Tabela de símbolos Contem as restantes labels que não estão definidas, nomeadamente referências externas. Informação de debug Contem uma descrição de como os módulos foram compilados, permitindo aos debuggers associar o código máquina aos ficheiro em C, mantendo as estruturas de dados legíveis. Ricardo Mendão Silva [email protected] Índice • Traduzindo e iniciando uma aplicação • • • • • • Compiladores Assembladores Linkers Loaders DLLs Iniciando um programa em Java Ricardo Mendão Silva [email protected] Traduzindo uma aplicação Linker • O método que apresenta-mos leva-nos a querer que cada linha de código alterada obriga à recompilação de todo o código. • Essa recompilação seria um gasto de recursos pesadíssimo, principalmente na compilação de bibliotecas, cujas rotinas quase nunca mudam. • Como tal, a alternativa é compilar cada procedimento independente, de modo a que a alteração de uma linha obrigue somente à recompilação de um procedimento. • Esta alternativa obriga à existência de um novo interveniente chamado linker ou link editor. • O Linker basicamente pega em todos os blocos compilados e aglomera-os numa solução única Ricardo Mendão Silva [email protected] Traduzindo uma aplicação Linker • Existem três etapas no modo de funionamento do Linker: 1. 2. 3. Coloca o código e os dados simbolicamente em memória. Determina os endereços das labels de dados e de instruções. Junta as referências internas com as externas. • O Linker utiliza a informação de realocação e a tabela de simbolos em cada objecto para resolver qualquer label indefinida, tal como, branches, jumps e endereços de memória. • O Linker funciona assim como um editor que procura os novos endereços e substitui pelos antigos. • É muito mais rápido aplicar um patch do que recompilar ou reassemblar todo o código. Ricardo Mendão Silva [email protected] Traduzindo uma aplicação Linker • Quando todas as referências externas estiverem resolvidas, o Linker determina as localização de memória que cada modulo irá ocupar. • Uma vez que os ficheiros são essemblados isoladamente, o assemblador não sabe onde as instruções e os dados de um modulo são colocados relativamente aos restantes. • Assim, quando o Linker coloca um modulo em memória, todas as referências absolutas (endereços de memória não relativos a registos) são realocadas. • Por fim, o Linker produz um ficheiro executável que pode correr num computador. Tipicamente, este executável tem o mesmo formato que o ficheiro objecto, mas sem referências indefinidas. Ricardo Mendão Silva [email protected] Traduzindo uma aplicação Linker Ricardo Mendão Silva [email protected] Traduzindo uma aplicação Linker Ricardo Mendão Silva [email protected] Índice • Traduzindo e iniciando uma aplicação • • • • • • Compiladores Assembladores Linkers Loaders DLLs Iniciando um programa em Java Ricardo Mendão Silva [email protected] Traduzindo uma aplicação Loader • Uma vez que o executável já se encontra no disco o Sistema Operativo lê o mesmo para memória e inicia-o, seguindo os seguintes passos: 1. Lê o cabeçalho para determinar o tamanho do texto e dos segmentos de dados. 2. Aloca memória necessária para o texto e dados. 3. Copia a instrução e os dados do executável para memória. 4. Copia os parâmetros (se existirem) do programa para a stack. 5. Inicia os registos e coloca o stack pointer a apontar para a primeira localização livre. 6. Salta para uma rotina de iniciação que copia os parâmetros para os registos de argumentos e chama a rotina principal do programa. 7. Quando a rotina principal retorna, a rotina de iniciação termina o programa com um exit system call. Ricardo Mendão Silva [email protected] Índice • Traduzindo e iniciando uma aplicação • • • • • • Compiladores Assembladores Linkers Loaders DLLs Iniciando um programa em Java Ricardo Mendão Silva [email protected] Traduzindo uma aplicação Dynamically Linked Libraries • Como vimos anteriormente, as bibliotecas podem ser de dois tipos: estáticas e dinâmicas. • Bibliotecas estáticas continuam a ser a forma mais rápida de chamar rotinas, no entanto têm várias desvantagens, nomeadamente: • A biblioteca faz parte do código, logo se uma nova versão da biblioteca for lançada o código manterá em uso a versão antiga até ser novamente compilado. • Todas as rotinas são carregadas no executável, independentemente se são ou não utilizadas. Isto provoca executáveis densos não por causa do próprio código, mas devido às bibliotecas que incluem. • Tais desvantagens levaram a que surgissem bibliotecas dinâmicas, ou popularmente conhecidas como dynamically linked libraries (DLLs). • Nas DLLs as rotinas não são nem ligadas nem carregadas até o programa iniciar a sua execução. Ricardo Mendão Silva [email protected] Traduzindo uma aplicação Dynamically Linked Libraries • Como tal, as DLLs são bibliotecas, cujas rotinas são carregadas durante a execução do programa, conforme necessário. • Inicialmente, nas primeiras versões das DLL, o Loader corria um Linker dinâmico que, com base na informação extra existentes, achava as bibliotecas necessárias e actualizava as referências externas. • A desvantagem deste método era que ele continuava a ligar todas as rotinas que poderiam ser chamadas, em vez de ligar somente as que eram mesmo chamadas durante a execução. • Para resolver essa questão foi desenvolvida uma segunda versão de DLLs onde cada rotina somente é ligada depois de ser invocada (lazy procedure linkage). Ricardo Mendão Silva [email protected] Traduzindo uma aplicação Dynamically Linked Libraries • Este processo é baseado num salto incondicional. Na primeira execução o comando é passado para o loader/linker que liga e carrega a rotina desejada e só no fim de carregada o comando volta à execução da instrução em causa. Nas instruções seguintes a rotina já está carregada e pode ser utilizada directamente. Ricardo Mendão Silva [email protected] Índice • Traduzindo e iniciando uma aplicação • • • • • • Compiladores Assembladores Linkers Loaders DLLs Iniciando um programa em Java Ricardo Mendão Silva [email protected] Traduzindo uma aplicação Iniciando um programa em Java • O exemplo que vimos anteriormente funciona para ISAs especificas e é utilizado quando pretendemos máxima performance sobre plataformas especificas de hardware. • Em Java o cenário muda um pouco, sendo uma plataforma desenhada para correr em qualquer hardware. • Em vez de ser compilado para uma qualquer versão de assembly, o código Java é primariamente compilado para instruções mais fáceis de interpretar numa arquitectura chamada Java bytecode instruction set. • Esta arquitectura foi desenhada para ser aproximada da linguagem Java, permitindo assim uma compilação fácil. Ricardo Mendão Silva [email protected] Traduzindo uma aplicação Iniciando um programa em Java • Tal como no C, o compilador Java verifica os tipos de dados e produz as operações necessárias para cada tipo. • Os programas em java são distribuídos na versão binária de bytecodes. Ricardo Mendão Silva [email protected] Traduzindo uma aplicação Iniciando um programa em Java • Um software de interpretação chamado Java Virtual Machine (JVM) consegue executar Java bytecodes. • Um interpretador é um programa que simula uma instruction set architecture. • Nestes casos não existe a necessidade de utilizar um assemblador separado, visto que a tradução é tão simples que o próprio compilador consegue obter os endereços em falta e/ou a JVM encontra-los em runtime. Ricardo Mendão Silva [email protected] Traduzindo uma aplicação Iniciando um programa em Java • A vantagem deste método é a portabilidade. Uma vez instalada a JVM, qualquer máquina pode corre aplicações escritas em Java. • A desvantagem deste tipo de soluções é claramente a performance, apesar de os últimos avanços na tecnologias não o fazerem transparecer tanto. Porém, a performance continua ainda a ser 10 vezes mais lenta quando comparada com aplicações em C. Ricardo Mendão Silva [email protected] Traduzindo uma aplicação Iniciando um programa em Java • Para preservar a portabilidade e melhorar a velocidade de execução, a fase seguinte no Java passou pelo desenvolvimento de compiladores que traduziam em runtime – Just in Time compilers. • Este tipo de compiladores criava um perfil do programa através dos métodos mais importantes. A solução passava depois por compilar esses métodos mais usados para linguagens máquina, tornando-os assim mais optimizados. Ao mesmo tempo guardava essa compilação para que nas execuções futuras fosse mais rápido. Ricardo Mendão Silva [email protected] Dúvidas e Questões Ricardo Mendão Silva Maio 2014 [email protected] Ricardo Mendão Silva [email protected]