2.2 Implementação do Modelo em Uma Arquitetura
2.2.6 Chamadas Recursivas
Para finalizar o estudo sobre o funcionamento de chamadas de procedimento, esta seção apresenta um exem- plo de um programa recursivo. A idéia é fixar todos os conceitos apresentados sobre chamadas de procedi- mento, em especial a forma com que os registros de ativação são empilhados para um mesmo procedimento (ou seja, como é a implementação de várias folhas de papel sobrepostas que correspondem a um mesmo procedimento).
O algoritmo 25 contém todos estes elementos. Ele calcula o fatorial de um número (no caso, de 4). Existem maneiras muito mais elegantes e muito mais eficientes para implementar um programa que calcula o fatorial, porém este programa foi implementado desta maneira (até certo ponto confusa) para incluir parâ- metros passados por referência, parâmetros passados por valor e variáveis locais (observe que a variável “r” é desnecessária).
A tradução do algoritmo 25 é apresentada no algoritmo 26.
Os aspectos importantes a serem destacados neste algoritmo são os seguintes: • a forma de inserir o endereço de x na pilha (linhas 44 a 46).
• a forma de acessar o valor de x (através do endereço que está na pilha (*x): linhas 27 e 28. • os passos para a chamada recursiva (linhas 21 a 25).
2.2. IMPLEMENTAÇÃO DO MODELO EM UMA ARQUITETURA
45
void fat ( int* res, int n)
1{
2int r;
3if (n<=1)
4*res=1;
5else {
6fat (res, n-1);
7r = *res * n;
8*res=r;
9}
10}
11main (int argc, char** argv)
12{
13int x;
14fat (&x, 4);
15return (x);
16}
17Algoritmo 25: Programa Recursivo.
46
CAPÍTULO 2. A SEÇÃO DA PILHA
.section .data
1.section .text
2.globl _start
3fat:
4pushl %ebp
5movl %esp, %ebp
6subl $4, %esp
7movl 12(%ebp), %eax
8movl $1, %ebx
9cmpl %eax, %ebx
10jl else
11movl 8(%ebp), %eax
12movl $1, (%eax)
13jmp fim_if
14else:
15movl 12(%ebp), %eax
16subl $1, %eax
17pushl %eax
18pushl 8(%ebp)
19call fat
20addl $8, %esp
21movl 8(%ebp), %eax
22movl (%eax), %eax
23movl 12(%ebp), %ebx
24imul %ebx, %eax
25movl %eax, -4(%ebp)
26movl -4(%ebp), %eax
27movl 8(%ebp), %ebx
28movl %eax, (%ebx)
29fim_if:
30addl $4, %esp
31popl %ebp
32ret
33_start:
34pushl %ebp
35movl %esp, %ebp
36subl $4, %esp
37pushl $4
38movl %ebp, %eax
39subl $4, %eax
40pushl %eax
41call fat
42addl $8, %esp
43movl -4(%ebp), %ebx
44movl $1, %eax
45int $0x80
462.2. IMPLEMENTAÇÃO DO MODELO EM UMA ARQUITETURA
47
A figura 2.3 detalha o conteúdo de um registro de ativação. Observe que%ebpaponta para o endereço de memória que contém o valor de%ebpdo registro de ativação anterior. Nesta figura, fica mais fácil de perceber qual o endereço relativo a%ebpdos parâmetros e das variáveis locais.A figura 2.4 mostra uma série de registros de ativação empilhados, como seria na execução do programa recursivo tratado nesta seção. Cada registro de ativação está representado com uma cor diferente. Da es- querda para a direita, a figura mostra como os registros de ativação são colocados na pilha a cada chamada de procedimento. A configuração mais à equerda é obtida no procedimento “main”, a configuração à direita corresponde à chamada de fat(4), e à esquerda desta, correspode a fat(3), e assim por diante até fat(1).
O objetivo desta figura é ressaltar os valores de%ebpao longo do tempo. Ele sempre aponta para o registro de ativação corrente, no endereço exato onde está guardado o registro de ativação do procedimento anterior a este. As setas indicam a lista encadeada de valores de%ebp.
A figura mostra que o registrador%ebpaponta para último registro de ativação colocado na pilha. O endereço apontado por ele é o local onde o valor anterior de%ebpfoi salvo (veja figura 2.3). O valor antigo de%ebpaponta para outro registro de ativação (imediatamente acima). Este, por sua vez, aponta para o registro de ativação anterior, e assim por diante. O último registro de ativação corresponde ao procedimento
mainconforme destacado na figura.
Figura 2.4: Registros de ativação do procedimento “fat” ao longo das chamadas recur-
sivas.
Como exercício, indique:
1. quais instruções assembly são responsáveis pelo empilhamento e pelo desempilhamento de cada campo do registro de ativação.
48
CAPÍTULO 2. A SEÇÃO DA PILHA
2. preencha os valores das variáveis do programa em “C” nos campos relacionados com as variáveis na figura 2.4.
2.2.7
Uso de Bibliotecas
Os programas apresentados até o momento ou não imprime os resultados, ou os coloca em uma variável de ambiente. Agora descreveremos como trabalhar com funções desenvolvidas externamente, mais especifica- mente com as funções disponíveis na libc, em especial as funções printf e scanf.
Para utilizar estas duas funções, são necessários dois cuidados:
1. Dentro do programa assembly, empilhar os procedimentos como descrito neste capítulo.
2. Ao ligar o programa, é necessário incluir a própria libc. Como faremos a ligação dinâmica, é neces- sário incluir a biblioteca que contém o ligador dinâmico.
Para exemplificar o processo, considere o algoritmo 27, e sua tradução, o programa28.
main (int argc, char** argv)
1{
2int x, y;
3scanf ("Digite dois numeros %d %d ", &x, &y);
4printf("Os numeros digitados foram %d %d\n ", x, y);
5
}
6Algoritmo 27: Uso de printf e scanf.
Como pode ser observado, a tradução é literal. Empilha-se os parâmetros e em seguida há a chamada para as funções printf e scanf.
> as psca.s -o psca.o
> ld psca.o -o psca -lc -dynamic-linker /lib/ld-linux.so.2 > ./psca
> Digite dois números: 1 2 > Os numeros digitados foram 1 2
As funçõesprintfescanfnão estão implementadas aqui, e sim na biblioteca libc, disponível em /usr/lib/libc.a (versão estática) e /usr/lib/libc.so (versão dinâmica). Como é mais conveniente utilizar a bibli- oteca dinâmica, usaremos esta para gerar o programa executável.
A opção-dynamic-linker /lib/ld-linux.so.2indica qual o ligador dinâmico que será o responsável por carregar a bibliotecalibc, indicada em-lc, em tempo de execução.
2.3
Aspetos de Segurança
A pilha já foi alvo de ataques, e foi considerada um ponto vulnerável em sistemas Unix (e conseqüentemente linux). Esta falha de vulnerabilidade era conseqüência da implementação de família de funçõesgetc, getchar, fgetc, que não verificam violações do espaço alocado para as variáveis destino.
Como exemplo, veja o algoritmo 29.
A primeira coisa interessante é que ao compilar este programa, surge uma mensagem curiosa:
> gcc get.c
/tmp/ccaXi9xy.o: In function ‘main’:
get.c:(.text+0x1f): warning: the ‘gets’ function is dangerous and should not be used.
O aviso é que a função “gets” é perigosa e que não deve ser usada. O porquê deste perigo é que ela não verifica os limites do vetor (que no caso tem cinco bytes). Para demonstrar o problema, veja o que ocorre com a execução onde a entrada de dados é maior do que o tamanho do vetor:
2.3. ASPETOS DE SEGURANÇA
49
.section .data
1str1: .string "Digite dois números: "
2str2: .string "%d %d"
3str3: .string "Os numeros digitados foram %d %d\n"
4
.section .text
5.globl _start
6_start:
7pushl %ebp
8movl %esp, %ebp
9subl $8, %esp
10pushl $str1
11call printf
12addl $4, %esp
13movl %ebp, %eax
14subl $8, %eax
15pushl %eax
16movl %ebp, %eax
17subl $4, %eax
18pushl %eax
19pushl $str2
20call scanf
21addl $12, %esp
22pushl -8(%ebp)
23pushl -4(%ebp)
24pushl $str3
25call printf
26addl $12, %esp
27movl $1, %eax
28int $0x80
29Algoritmo 28: Tradução do Algoritmo 27
main ( int argc, char** argv);
1{
2char s[5];
3gets (s); printf("}
450
CAPÍTULO 2. A SEÇÃO DA PILHA
> ./get 1234567890 1234567890
Observe que foram digitados 10 caracteres, e eles foram armazenados (com sucesso) no vetor. Eviden- temente eles não “cabem” no espaço alocado para eles, e podem sobreescrever outras variáveis.
Estas variáveis são alocadas na pilha, e o primeiro artigo que citou esta vulnerabilidade foi no artigo postado na internet “Smashing The Stack For Fun And Profit” de Aleph One.
Este ataque ocorre quando o usuário entra com uma quantidade de dados muito maior do que o tamanho da variável, sobreescrevendo outros dados na pilha, como o endereço de retorno do registro de ativação corrente.
Se os dados digitados incluírem um trecho de código (por exemplo, o código de /usr/bin/sh) será injetado um trecho de código na pilha. O próximo passo envolve o comando assemblyret. Se antes desta instrução contiver o endereço do código injetado, então o código injetado será executado.
Agora, vamos analisar o que ocorreria na injeção em um processo que executa como super-usuário, como por exemplo, oftpem linha de comando.
Se o código injetado for osh, o usuário não só executará a shell, mas a executará como super-usuário! Mais do que simples conjecturas, este método foi usado (e com sucesso) durante algum tempo. Para mais detalhes, veja [JDD+04].
Desde a publicação do artigo e da detecção da falha de segurança, foram criados mecanismos para evitar este tipo de invasão, pode ser visto na execução do algoritmo 29, com maior quantidade de dados.
./get
10101010101010101010101010110
*** stack smashing detected ***: ./get terminated ======= Backtrace: =========
Capítulo 3
Chamadas de Sistema
No modelo ELF, um programa em execução não pode acessar nenhum recurso externo à sua memória virtual. Aliás, existem regiões dentro de sua própria área virtual que o programa também não pode acessar, como pôde ser visto na figura 1.
Porém, praticamente todos os processo precisam acessar dados que estão fora de sua área virtual, como arquivos, eventos do mouse e teclados, entre várias outras.
No ELF, o mecanismo que permite acessar estes recursos utiliza “chamadas ao sistema” (system calls). Este capítulo descreve o que são e como funcionam as chamadas ao sistema no linux em especial em máquinas x86. Como o conceito de chamadas ao sistema ser universal, também apresentamos o modelo que foi adotado no MS-DOS. O objetivo é mostrar que as chamadas ao sistema, se mal utilizadas, podem causar sérios problemas ao computador.
Explicar o que são e como funcionam as chamadas ao sistema na prática envolve conhecimentos maiores de hardware, e para manter uma abordagem mais “leve”, apresentamos algumas analogias.
A primeira analogia é descrita na seção 3.1, e explica os modos de execução de um processo, usuário e supervisor. Em seguida, a seção 3.2 acrescenta CPU e a seção 3.3 acrescenta os periféricos (dispositivos de hardware) à analogia.
Com a analogia completa, a seção3.4 explica como as coisas ocorrem na prática em uma arquitetura. Em seguida, a seção 3.6 apresenta uma outra forma de implementar as chamadas ao sistema (MS-DOS), e relata alguns problemas que podem ser gerados com esta implementação. Por fim, a seção 3.7 apresenta as chamadas ao sistema no linux.
3.1
A analogia do parquinho
Uma analogia que eu gosto de usar é a de que um processo em execução em memória virtual é semelhante a uma criança dentro de uma “caixa de areia” em um parquinho de uma creche.
Imagine uma creche onde as crianças são colocadas em áreas delimitadas para brincar, uma criança por área. Normalmente isto acontece em creches, onde estas áreas são “caixas de areia”.
Porém nesta analogia, ao contrário do que ocorre me creches, cada criança é confinada a uma única área, não podendo sair dali para brincar com outras crianças.
Agora, considere uma criança brincando em uma destas áreas. O que ela tem para brincar é aquilo que existe naquela caixa de areia. Ali, ela pode fazer o que quiser: jogar areia para cima, colocar areia na boca (e outras coisas que prefiro não descrever).
Tudo o que a criança fizer naquela caixa terá conseqüências unicamente para ela, ou seja, não afeta as crianças em outras caixas de areia.
Normalmente, só há uma criança por caixa de areia. Quando a criança terminar de brincar, a caixa de areia é destruída. Para cada criança que quiser brincar mas não estiver em uma caixa de areia, uma nova caixa será construída para ela, possivelmente usando areia de caixas destruídas anteriormente.
52
CAPÍTULO 3. CHAMADAS DE SISTEMA
Porém, é importante garantir que as “lembranças” deixadas por uma criança não sejam passadas para outras crianças. Uma forma de fazer isso, é esterilizando todo o material das caixas de areia destruídas, como por exemplo esterilizando a areia utilizada.
Quem faz esta tarefa é a “tia”, que aqui é conhecida como “supervisora”. Aliás, é bom destacar que é isto que se espera de uma creche bem conceituada: que cuide da saúde das crianças. Você deixaria seu filho em um parquinho onde a supervisora negligencia a saúde das crianças?
Esta supervisora também é responsável por outras tarefas, como atender às requisições das crianças por dispositivos externos.
Suponha que existem brinquedos para as crianças usarem nas caixas de areia: baldinho, regador, etc.. Como as crianças não podem sair de sua caixa de areia, elas tem de pedir o brinquedo para a supervisora.
A supervisora recebe o pedido, e procura por algum brinquedo disponível. Eventualmente irá encontrar (nem que seja tirando outra caixa de areia onde está outra criança), porém antes de passar para a criança que solicitou, deve primeiramente higienizar o brinquedo para que “lembranças” deixadas pela primeira criança não possam “ser usadas” pela segunda criança.
Nesta analogia, temos que:
• crianças e supervisora são os processos;
• crianças tem uma área restrita de ação, enquanto que a supervisora tem uma área de atuação quase total;
• cada caixa de areia é a memória virtual de um processo.
• os brinquedos são recursos, como por exemplo dados de arquivo, de teclado, de mouse, etc.. Ou seja: são os objetos que um processo não pode acessar porque estão fora de sua memória virtual; • os pedidos e devoluções de recursos externos são executados através das chamadas ao sistema (system
calls).
Um detalhe importante é que crianças e supervisores, apesar de serem processos atuam em “modos” diferentes. Enquanto uma criança só pode acessar a sua caixa de areia e as coisas que estiverem lá dentro, a supervisora pode acessar qualquer caixa de areia, os espaços entre as caixas de areia e até fora delas (como por exemplo, em sua sala particular). Estes dois modos são conhecidos como “modo protegido” (para as crianças) e “modo supervisor” (para a supervisora).
Assim, as chamadas ao sistema são basicamente requisições que o processos fazem ao sistema operaci- onal por recursos externos à sua área virtual. Para acessar qualquer coisa que estiver fora de sua área virtual, o processo deve fazer uma chamada ao sistema.
3.2
Acrescentando a CPU
Na analogia da seção anterior, todos os “processos” (crianças e supervisora) podem trabalhar em paralelo, ou seja, duas crianças podem brincar em suas caixas de areia simultaneamente.
Porém, em uma arquitetura real, somente os processo que estão usando a CPU é que estão ativos. Os demais estão “em espera”.
Para explicar isso na analogia do parquinho, vamos introduzir um pouco de magia.
Uma moradora vizinha não gosta do barulho que as crianças fazem quando estão brincando. Para azar de todos, ela é uma bruxa malvada que invocou uma maldição terrível para silenciar todos.
Esta maldição ilumina todo o parquinho com uma luz cinza que tranforma todos os atingidos em estátuas (inclusive a supervisora).
Outra vizinha é uma bruxa boazinha, que estranhando a falta de barulho, percebeu o que ocorreu. Ela não é uma bruxa tão poderosa quanto a bruxa malvada, e não pôde reverter o feitiço.
Mesmo assim, conseguiu conjurar uma fadinha voadora, imune à luz cinza. Quando ela sobrevoa uma criança (ou a supervisora) consegue “despertá-la” por algum tempo usando uma luz colorida. Infelizmente, a fadinha só consegue acordar exatamente uma pessoa por vez.
Por esta razão, a bruxa boazinha pediu para a fadinha não ficar muito tempo em uma única pessoa, e mudar de pessoa a pessoa para que todos pudessem brincar um pouco.
Porém, a bruxa boazinha observou que a fadinha não pensava muito para escolher a próxima pessoa para acordar, e com isso ela acabava acordando somente um grupo pequeno de pessoas (e outras não eram sequer acordadas).