• Nenhum resultado encontrado

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

{

2

int r;

3

if (n<=1)

4

*res=1;

5

else {

6

fat (res, n-1);

7

r = *res * n;

8

*res=r;

9

}

10

}

11

main (int argc, char** argv)

12

{

13

int x;

14

fat (&x, 4);

15

return (x);

16

}

17

Algoritmo 25: Programa Recursivo.

46

CAPÍTULO 2. A SEÇÃO DA PILHA

.section .data

1

.section .text

2

.globl _start

3

fat:

4

pushl %ebp

5

movl %esp, %ebp

6

subl $4, %esp

7

movl 12(%ebp), %eax

8

movl $1, %ebx

9

cmpl %eax, %ebx

10

jl else

11

movl 8(%ebp), %eax

12

movl $1, (%eax)

13

jmp fim_if

14

else:

15

movl 12(%ebp), %eax

16

subl $1, %eax

17

pushl %eax

18

pushl 8(%ebp)

19

call fat

20

addl $8, %esp

21

movl 8(%ebp), %eax

22

movl (%eax), %eax

23

movl 12(%ebp), %ebx

24

imul %ebx, %eax

25

movl %eax, -4(%ebp)

26

movl -4(%ebp), %eax

27

movl 8(%ebp), %ebx

28

movl %eax, (%ebx)

29

fim_if:

30

addl $4, %esp

31

popl %ebp

32

ret

33

_start:

34

pushl %ebp

35

movl %esp, %ebp

36

subl $4, %esp

37

pushl $4

38

movl %ebp, %eax

39

subl $4, %eax

40

pushl %eax

41

call fat

42

addl $8, %esp

43

movl -4(%ebp), %ebx

44

movl $1, %eax

45

int $0x80

46

2.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

{

2

int x, y;

3

scanf ("Digite dois numeros %d %d ", &x, &y);

4

printf("Os numeros digitados foram %d %d\n ", x, y);

5

}

6

Algoritmo 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

1

str1: .string "Digite dois números: "

2

str2: .string "%d %d"

3

str3: .string "Os numeros digitados foram %d %d\n"

4

.section .text

5

.globl _start

6

_start:

7

pushl %ebp

8

movl %esp, %ebp

9

subl $8, %esp

10

pushl $str1

11

call printf

12

addl $4, %esp

13

movl %ebp, %eax

14

subl $8, %eax

15

pushl %eax

16

movl %ebp, %eax

17

subl $4, %eax

18

pushl %eax

19

pushl $str2

20

call scanf

21

addl $12, %esp

22

pushl -8(%ebp)

23

pushl -4(%ebp)

24

pushl $str3

25

call printf

26

addl $12, %esp

27

movl $1, %eax

28

int $0x80

29

Algoritmo 28: Tradução do Algoritmo 27

main ( int argc, char** argv);

1

{

2

char s[5];

3

gets (s); printf("}

4

50

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).

3.3. ACRESENTANDO PERIFÉRICOS E DISPOSITIVOS DE HARDWARE

53