Projeto do compilador - ambiente de tempo de execução

Um programa como código-fonte é meramente uma coleção de texto (código, instruções etc.) e, para torná-lo vivo, requer que ações sejam executadas na máquina de destino. Um programa precisa de recursos de memória para executar as instruções. Um programa contém nomes para procedimentos, identificadores etc., que requerem mapeamento com a localização real da memória em tempo de execução.

Por tempo de execução, queremos dizer um programa em execução. O ambiente de tempo de execução é um estado da máquina de destino, que pode incluir bibliotecas de software, variáveis ​​de ambiente, etc., para fornecer serviços aos processos em execução no sistema.

O sistema de suporte de tempo de execução é um pacote, gerado principalmente com o próprio programa executável e facilita a comunicação do processo entre o processo e o ambiente de execução. Ele cuida da alocação e desalocação de memória enquanto o programa está sendo executado.

Árvores de Ativação

Um programa é uma sequência de instruções combinadas em vários procedimentos. As instruções em um procedimento são executadas sequencialmente. Um procedimento tem um delimitador de início e fim e tudo dentro dele é chamado de corpo do procedimento. O identificador do procedimento e a sequência de instruções finitas dentro dele constituem o corpo do procedimento.

A execução de um procedimento é chamada de ativação. Um registro de ativação contém todas as informações necessárias para chamar um procedimento. Um registro de ativação pode conter as seguintes unidades (dependendo do idioma de origem usado).

Temporários Armazena valores temporários e intermediários de uma expressão.
Dados Locais Armazena dados locais do procedimento chamado.
Status da máquina Armazena o status da máquina, como registros, contador de programa, etc., antes que o procedimento seja chamado.
Link de controle Armazena o endereço de registro de ativação do procedimento do chamador.
Link de acesso Armazena as informações de dados que estão fora do escopo local.
Parâmetros reais Armazena parâmetros reais, ou seja, parâmetros que são usados ​​para enviar entrada para o procedimento chamado.
Valor de retorno Armazena valores de retorno.

Sempre que um procedimento é executado, seu registro de ativação é armazenado na pilha, também conhecida como pilha de controle. Quando um procedimento chama outro procedimento, a execução do chamador é suspensa até que o procedimento chamado termine a execução. Nesse momento, o registro de ativação do procedimento chamado é armazenado na pilha.

Assumimos que o controle do programa flui de forma sequencial e quando um procedimento é chamado, seu controle é transferido para o procedimento chamado. Quando um procedimento chamado é executado, ele retorna o controle ao chamador. Este tipo de fluxo de controle torna mais fácil representar uma série de ativações em forma de árvore, conhecida comoactivation tree.

Para entender esse conceito, tomamos um trecho de código como exemplo:

. . .
printf(“Enter Your Name: “);
scanf(“%s”, username);
show_data(username);
printf(“Press any key to continue…”);
. . .
int show_data(char *user)
   {
   printf(“Your name is %s”, username);
   return 0;
   }
. . .

Abaixo está a árvore de ativação do código fornecido.

Agora entendemos que os procedimentos são executados em primeiro lugar em profundidade, portanto, a alocação de pilha é a melhor forma adequada de armazenamento para ativações de procedimento.

Alocação de armazenamento

O ambiente de tempo de execução gerencia os requisitos de memória do tempo de execução para as seguintes entidades:

  • Code: É conhecido como a parte de texto de um programa que não muda durante a execução. Seus requisitos de memória são conhecidos no momento da compilação.

  • Procedures: Sua parte do texto é estática, mas eles são chamados de maneira aleatória. É por isso que o armazenamento em pilha é usado para gerenciar chamadas e ativações de procedimento.

  • Variables: As variáveis ​​são conhecidas apenas no tempo de execução, a menos que sejam globais ou constantes. O esquema de alocação de memória heap é usado para gerenciar a alocação e desalocação de memória para variáveis ​​em tempo de execução.

Alocação estática

Nesse esquema de alocação, os dados de compilação são vinculados a um local fixo na memória e não mudam quando o programa é executado. Como o requisito de memória e os locais de armazenamento são conhecidos com antecedência, o pacote de suporte de tempo de execução para alocação e desalocação de memória não é necessário.

Alocação de pilha

As chamadas de procedimento e suas ativações são gerenciadas por meio da alocação de memória da pilha. Ele funciona no método LIFO (last-in-first-out) e essa estratégia de alocação é muito útil para chamadas de procedimento recursivas.

Alocação de heap

Variáveis ​​locais para um procedimento são alocadas e desalocadas apenas em tempo de execução. A alocação de heap é usada para alocar memória dinamicamente às variáveis ​​e reivindicá-la de volta quando as variáveis ​​não forem mais necessárias.

Exceto a área de memória alocada estaticamente, as memórias de pilha e heap podem aumentar e diminuir de forma dinâmica e inesperada. Portanto, eles não podem ser fornecidos com uma quantidade fixa de memória no sistema.

Conforme mostrado na imagem acima, a parte do texto do código é alocada a uma quantidade fixa de memória. A pilha e a memória heap são organizadas nos extremos da memória total alocada ao programa. Ambos encolhem e crescem um contra o outro.

Passagem de parâmetro

O meio de comunicação entre procedimentos é conhecido como passagem de parâmetros. Os valores das variáveis ​​de um procedimento de chamada são transferidos para o procedimento chamado por algum mecanismo. Antes de prosseguir, primeiro analise algumas terminologias básicas relativas aos valores em um programa.

valor r

O valor de uma expressão é chamado de valor r. O valor contido em uma única variável também se torna um valor r se aparecer no lado direito do operador de atribuição. Os valores r podem sempre ser atribuídos a alguma outra variável.

valor-l

A localização da memória (endereço) onde uma expressão é armazenada é conhecida como o valor l dessa expressão. Ele sempre aparece no lado esquerdo de um operador de atribuição.

Por exemplo:

day = 1;
week = day * 7;
month = 1;
year = month * 12;

A partir deste exemplo, entendemos que valores constantes como 1, 7, 12 e variáveis ​​como dia, semana, mês e ano, todos têm valores r. Apenas as variáveis ​​têm valores-l, pois também representam a localização da memória atribuída a elas.

Por exemplo:

7 = x + y;

é um erro de valor l, pois a constante 7 não representa nenhum local da memória.

Parâmetros Formais

As variáveis ​​que recebem as informações passadas pelo procedimento do chamador são chamadas de parâmetros formais. Essas variáveis ​​são declaradas na definição da função chamada.

Parâmetros reais

Variáveis ​​cujos valores ou endereços estão sendo passados ​​para o procedimento chamado são chamadas de parâmetros reais. Essas variáveis ​​são especificadas na chamada de função como argumentos.

Example:

fun_one()
{
   int actual_parameter = 10;
   call fun_two(int actual_parameter);
}
   fun_two(int formal_parameter)
{
   print formal_parameter;
}

Os parâmetros formais contêm as informações do parâmetro real, dependendo da técnica de passagem de parâmetro usada. Pode ser um valor ou um endereço.

Passe por valor

No mecanismo de passagem por valor, o procedimento de chamada passa o valor r dos parâmetros reais e o compilador coloca isso no registro de ativação do procedimento chamado. Parâmetros formais então contêm os valores passados ​​pelo procedimento de chamada. Se os valores mantidos pelos parâmetros formais forem alterados, isso não deve ter impacto nos parâmetros reais.

Passe por referência

No mecanismo de passagem por referência, o valor l do parâmetro real é copiado para o registro de ativação do procedimento chamado. Desta forma, o procedimento chamado passa a ter o endereço (local da memória) do parâmetro atual e o parâmetro formal refere-se ao mesmo local da memória. Portanto, se o valor apontado pelo parâmetro formal for alterado, o impacto deve ser visto no parâmetro real, pois eles também devem apontar para o mesmo valor.

Passe por cópia-restauração

Este mecanismo de passagem de parâmetro funciona de forma semelhante à 'passagem por referência', exceto que as alterações nos parâmetros reais são feitas quando o procedimento chamado termina. Na chamada da função, os valores dos parâmetros reais são copiados no registro de ativação do procedimento chamado. Parâmetros formais, se manipulados, não têm efeito em tempo real sobre os parâmetros reais (conforme os valores l são passados), mas quando o procedimento chamado termina, os valores l dos parâmetros formais são copiados para os valores l dos parâmetros reais.

Example:

int y; 
calling_procedure() 
{
   y = 10;     
   copy_restore(y); //l-value of y is passed
   printf y; //prints 99 
}
copy_restore(int x) 
{     
   x = 99; // y still has value 10 (unaffected)
   y = 0; // y is now 0 
}

Quando esta função termina, o valor l do parâmetro formal x é copiado para o parâmetro real y. Mesmo que o valor de y seja alterado antes de o procedimento terminar, o valor l de x é copiado para o valor l de y, fazendo com que se comporte como uma chamada por referência.

Passe por Nome

Linguagens como Algol fornecem um novo tipo de mecanismo de passagem de parâmetro que funciona como um pré-processador na linguagem C. No mecanismo de passagem por nome, o nome do procedimento que está sendo chamado é substituído por seu corpo real. A passagem por nome substitui textualmente as expressões de argumento em uma chamada de procedimento para os parâmetros correspondentes no corpo do procedimento para que agora ele possa funcionar em parâmetros reais, bem como passagem por referência.