Neste artigo vamos explorar o gerenciamento de memória no Linux para entendermos como funciona a paginação e explorar as funções de acesso a memória do espaço do usuário que usam estes conceitos. Movimentar dados entre espaço de usuário e kernel. Vamos usar tmb APIs.
A capacidade de definir memória para processos de maneira esparsa significa que a memória física subjacente pode ser confirmada em excesso. Por meio de um processo chamado paginação (embora, no Linux, seja geralmente chamado de troca), páginas pouco usadas são movidas dinamicamente para um dispositivo de armazenamento mais lento (por exemplo, um disco) para acomodar outras páginas que precisam ser acessadas (veja a Figura 2). Isso permite que a memória física do computador forneça páginas que um aplicativo precisa imediatamente, e migre páginas menos necessárias para o disco para melhor uso da memória física. Observe que algumas páginas podem se referir a arquivos, caso no qual os dados podem ser descarregados se a página estiver suja (por meio do cache de página) ou, se a página estiver limpa, simplesmente descartados.
O processo pelo qual uma página é selecionada para ser descarregada para o armazenamento é chamado de algoritmo de substituição de página e pode ser implementado usando alguns algoritmos diferentes (como, por exemplo, usado menos recentemente). Esse processo pode ocorrer quando é solicitado um local de memória cuja página não está na memória (não há mapeamento presente na memory management unit [MMU]). Esse evento é chamado de falha de página e é detectado pelo hardware (a MMU) e gerenciado pelo firmware após uma interrupção de falha de página ocorrer. A Figura 3 contém uma ilustração dessa pilha.
O Linux apresenta uma implementação interessante da troca, que oferece algumas características úteis. O sistema de troca do Linux permite a criação e uso de várias partições e prioridades de troca, o que permite uma hierarquia de troca para dispositivos de armazenamento que fornecem diferentes características de desempenho (por exemplo, uma troca de primeiro nível em uma unidade de estado sólido [SSD] e um espaço de troca maior, de segundo nível, em um dispositivo de armazenamento mais lento). Designar uma prioridade maior à troca em SSD permite que ele seja usado até estar cheio; só então as páginas seriam gravadas na partição de troca de baixa prioridade (mais lenta).
Nem todas as páginas podem ser descarregadas para a área de troca. Por exemplo, código do kernel que responde a interrupções, ou código que gerencia as tabelas de página e a lógica de troca. Essas são páginas que, obviamente, não devem ser descarregadas para a área de troca, e portanto são fixadas, ou residentes permanentemente na memória. Embora páginas do kernel não possam ser descarregadas, páginas do espaço do usuário podem, mas é possível fixá-las através da função mlock (ou mlockall) para bloquear a página. Esse é o propósito das funções de acesso à memória do espaço do usuário. Se o kernel achasse que um endereço passado por um usuário é válido e acessível, ocorreria por fim um pânico do kernel (por exemplo, porque a página do usuário foi descarregada para a área de troca, resultando em uma falha de página no kernel). Essa interface de programação de aplicativo (API) assegura que tais casos sejam tratados corretamente.
O argumento type pode ser especificado como VERIFY_READ ou VERIFY_WRITE. O simbólico VERIFY_WRITE também identifica se a região da memória é legível, além de gravável. A função retorna diferente de zero se a região for provavelmente acessível (mas o acesso ainda pode resultar em -EFAULT). Essa função simplesmente verifica se o endereço é provável no espaço do usuário, não no kernel.
Assim como get_user, a função put_user é mapeada internamente para a função put_user_x, e retorna 0 em caso de sucesso ou -EFAULT em caso de erro.
Internamente, a função clear_user verifica primeiramente se o ponteiro do espaço do usuário é gravável (via access_ok), e em seguida chama uma função interna (escrita em assembly sequencial) para realizar a operação de Limpeza. Essa função está otimizada como um loop muito rígido usando instruções de cadeia de caractere com o prefixo de repetição. Ela retorna o número de bytes que não puderam ser limpos, ou zero caso a operação tenha tido sucesso.
A função começa por verificar a possibilidade de ler o buffer de origem no espaço do usuário (via access_ok), e em seguida chama __copy_from_user e, por fim, __copy_from_user_ll. A partir daí, dependendo da arquitetura, uma chamada é feita para copiar do buffer do usuário para um buffer do kernel zerando (os bytes indisponíveis). As funções de assembly otimizadas incluem a capacidade de gerenciar.
A função strnlen_user verifica primeiramente se o buffer do usuário é legível, por meio de uma chamada para access_ok. Se for acessível, a função strlen é chamada, e o argumento max length é ignorado.
Como se trata de uma cópia do espaço do usuário, a função primeiramente verifica se o buffer é gravável por meio de access_ok. De forma semelhante a copy_from_user, essa função é implementada como uma função assembly otimizada (em ./linux/arch/x86/lib/usercopy_XX.c).
Liguem suas VMs!
Uma introdução à memória e APIs de espaço do usuário do Linux
Como o kernel e o espaço do usuário existem em espaços de endereço virtuais diferentes, existem considerações especiais para movimentar dados entre eles. Explore as ideias por trás dos espaços de endereço virtuais e as APIs do kernel para movimentar dados do e para o espaço do usuário, e aprenda algumas das outras técnicas de mapeamento usadas para mapear memória. Embora o byte seja a menor unidade de memória que pode ser endereçada no Linux, é a página que serve como a abstração gerenciada da memória. Este artigo começa com uma discussão sobre o gerenciamento da memória no Linux e, em seguida, explora os métodos para manipular o espaço de endereço a partir do kernel.
Memória no Linux
No Linux, a memória do usuário e a memória do kernel são independentes e implementadas em espaços de endereço separados. Os espaços de endereço são virtualizados, ou seja, os endereços são abstraídos da memória física (por meio de um processo que será detalhado mais adiante). Como os espaços de endereço são virtualizados, podem existir vários deles. Na verdade, o kernel reside em um espaço de endereço, e cada processo reside em seu próprio espaço. Esses espaços consistem em endereços de memória virtual, permitindo que vários processos com espaços de endereço independentes se refiram a um espaço de endereço físico consideravelmente menor (a memória física da máquina). Isso não é apenas conveniente, como também é seguro, pois cada espaço de endereço está isolado e, consequentemente, protegido.
Mas essa segurança tem um preço. Como cada processo (e o kernel) pode ter endereços idênticos que se referem a regiões diferentes da memória física, não é imediatamente possível compartilhar memória. Felizmente, existem algumas soluções. Processos do usuário podem compartilhar memória por meio do mecanismo de memória compartilhada da Portable Operating System Interface for UNIX® (POSIX) (shmem), com a advertência de que cada processo pode ter um endereço virtual diferente que se refere à mesma região da memória física.
O mapeamento da memória virtual para a memória física ocorre por meio de tabelas de página, implementadas no hardware subjacente. O próprio hardware fornece o mapeamento, mas é o kernel que gerencia as tabelas e sua configuração. Observe que, como mostrado aqui, um processo pode ter um grande espaço de endereço, mas é esparso, ou seja, pequenas regiões (páginas) do espaço se referem à memória física através das tabelas de página. Isso permite que um processo tenha um enorme espaço de endereço que é definido apenas para as páginas necessárias em um dado momento.
Mas essa segurança tem um preço. Como cada processo (e o kernel) pode ter endereços idênticos que se referem a regiões diferentes da memória física, não é imediatamente possível compartilhar memória. Felizmente, existem algumas soluções. Processos do usuário podem compartilhar memória por meio do mecanismo de memória compartilhada da Portable Operating System Interface for UNIX® (POSIX) (shmem), com a advertência de que cada processo pode ter um endereço virtual diferente que se refere à mesma região da memória física.
O mapeamento da memória virtual para a memória física ocorre por meio de tabelas de página, implementadas no hardware subjacente. O próprio hardware fornece o mapeamento, mas é o kernel que gerencia as tabelas e sua configuração. Observe que, como mostrado aqui, um processo pode ter um grande espaço de endereço, mas é esparso, ou seja, pequenas regiões (páginas) do espaço se referem à memória física através das tabelas de página. Isso permite que um processo tenha um enorme espaço de endereço que é definido apenas para as páginas necessárias em um dado momento.
Figura 1. Tabelas de página fornecem o mapeamento de endereços virtuais para endereços físicos
Figura 2. A troca permite melhor uso do espaço da memória física ao migrar páginas pouco usadas para armazenamento mais lento e menos caro
O Linux apresenta uma implementação interessante da troca, que oferece algumas características úteis. O sistema de troca do Linux permite a criação e uso de várias partições e prioridades de troca, o que permite uma hierarquia de troca para dispositivos de armazenamento que fornecem diferentes características de desempenho (por exemplo, uma troca de primeiro nível em uma unidade de estado sólido [SSD] e um espaço de troca maior, de segundo nível, em um dispositivo de armazenamento mais lento). Designar uma prioridade maior à troca em SSD permite que ele seja usado até estar cheio; só então as páginas seriam gravadas na partição de troca de baixa prioridade (mais lenta).
Figura 3. Espaços de endereço e elementos de mapeamento de endereço virtual para físico
APIs do Kernel
Agora, vamos explorar as APIs do kernel para manipular a memória do usuário. Observe que esta seção cobre a interface do kernel e do espaço do usuário, mas a próxima explora mais algumas APIs de memória. As funções de acesso à memória do espaço do usuário que iremos explorar estão listadas na Tabela 1.
Tabela 1. A API de acesso à memória do espaço do usuário
Função Descrição
access_ok Verifica a validade do ponteiro de memória do espaço do usuário
get_user Obtém uma variável simples do espaço do usuário
put_user Envia uma variável simples para o espaço do usuário
clear_user Limpa ou zera um bloco no espaço do usuário
copy_to_user Copia um bloco de dados do kernel para o espaço do usuário
copy_from_user Copia um bloco de dados do espaço do usuário para o kernel
strnlen_user Obtém o tamanho de um buffer de cadeia de caractere no espaço do usuário
strncpy_from_user Copia uma cadeia de caractere do espaço de usuário para o kernel
Como seria de se esperar, a implementação dessas funções depende da arquitetura. Em arquiteturas x86, é possível achar essas funções e símbolos definidos em ./linux/arch/x86/include/asm/uaccess.h, com origem em ./linux/arch/x86/lib/usercopy_32.c e usercopy_64.c.
O papel das funções de movimentação de dados é mostrado na Figura 4 em relação aos tipos envolvidos na cópia (simples vs. agregado).
Função Descrição
access_ok Verifica a validade do ponteiro de memória do espaço do usuário
get_user Obtém uma variável simples do espaço do usuário
put_user Envia uma variável simples para o espaço do usuário
clear_user Limpa ou zera um bloco no espaço do usuário
copy_to_user Copia um bloco de dados do kernel para o espaço do usuário
copy_from_user Copia um bloco de dados do espaço do usuário para o kernel
strnlen_user Obtém o tamanho de um buffer de cadeia de caractere no espaço do usuário
strncpy_from_user Copia uma cadeia de caractere do espaço de usuário para o kernel
Como seria de se esperar, a implementação dessas funções depende da arquitetura. Em arquiteturas x86, é possível achar essas funções e símbolos definidos em ./linux/arch/x86/include/asm/uaccess.h, com origem em ./linux/arch/x86/lib/usercopy_32.c e usercopy_64.c.
O papel das funções de movimentação de dados é mostrado na Figura 4 em relação aos tipos envolvidos na cópia (simples vs. agregado).
Figura 4. Movimentação de dados usando a API de acesso à memória do espaço do usuário
A função access_ok
A função access_ok é usada para verificar a validade do ponteiro no espaço do usuário que deverá ser acessado. O responsável pela chamada fornece o ponteiro (que se refere ao início do bloco de dados), o tamanho do bloco e o tipo de acesso (se a área deve ser lida ou gravada). O protótipo de função é definido como:
access_ok( type, addr, size );
O argumento type pode ser especificado como VERIFY_READ ou VERIFY_WRITE. O simbólico VERIFY_WRITE também identifica se a região da memória é legível, além de gravável. A função retorna diferente de zero se a região for provavelmente acessível (mas o acesso ainda pode resultar em -EFAULT). Essa função simplesmente verifica se o endereço é provável no espaço do usuário, não no kernel.
A função get_user
Para ler uma variável simples do espaço do usuário, usa-se a função get_user. Ela é usada para tipos simples, tais como char e int, mas tipos de dados maiores, como estruturas, devem usar a função copy_from_user. O protótipo aceita uma variável (para armazenar os dados) e um endereço no espaço do usuário para a operação de leitura:
get_user( x, ptr );
A função put_user
A função put_user é usada para gravar uma variável simples do kernel para o espaço do usuário. Assim como get_user, ela aceita uma variável (que contém o valor a ser escrito) e um endereço no espaço do usuário como destino da gravação:
put_user( x, ptr );
Assim como get_user, a função put_user é mapeada internamente para a função put_user_x, e retorna 0 em caso de sucesso ou -EFAULT em caso de erro.
A função clear_user
A função clear_user é usada para zerar um bloco de memória no espaço do usuário. Ela toma um ponteiro no espaço do usuário e um tamanho para zerar, definido em bytes:
clear_user( ptr, n );
Internamente, a função clear_user verifica primeiramente se o ponteiro do espaço do usuário é gravável (via access_ok), e em seguida chama uma função interna (escrita em assembly sequencial) para realizar a operação de Limpeza. Essa função está otimizada como um loop muito rígido usando instruções de cadeia de caractere com o prefixo de repetição. Ela retorna o número de bytes que não puderam ser limpos, ou zero caso a operação tenha tido sucesso.
A função copy_to_user
A função copy_to_user copia um bloco de dados do kernel para o espaço do usuário. Ela aceita um ponteiro para um buffer no espaço do usuário, um ponteiro para um buffer no kernel e um comprimento definido em bytes. A função retorna zero em caso de sucesso, ou diferente de zero para indicar o número de bytes que não foram transferidos.
copy_to_user( to, from, n );Após verificar a possibilidade de gravar no buffer do usuário (através de access_ok), é chamada a função interna __copy_to_user, que, por sua vez, chama __copy_from_user_inatomic (em ./linux/arch/x86/include/asm/uaccess_XX.h, no qual XX é 32 ou 64, dependendo da arquitetura). A função (depois de determinar deve-se realizar cópias de 1, 2 ou 4 bytes) chama por fim __copy_to_user_ll, que é responsável pelo trabalho de verdade. Em hardware quebrado (antes do i486, quando o bit WP não era honrado em modo de supervisor), as tabelas de página podiam mudar a qualquer momento, exigindo que as páginas desejadas fossem fixadas na memória, de modo que não fossem descarregadas para a área de troca enquanto estavam sendo endereçadas. Depois do i486, o processo nada mais é que uma cópia otimizada.
A função copy_from_user
A função copy_from_user copia um bloco de dados do espaço do usuário para um buffer do kernel. Ela aceita um buffer de destino (no espaço do kernel), um buffer de origem (no espaço do usuário) e um comprimento definido em bytes. Assim como copy_to_user, a função retorna zero em caso de sucesso, e diferente de zero para indicar o fracasso em copiar certo número de bytes.copy_from_user( to, from, n );
A função começa por verificar a possibilidade de ler o buffer de origem no espaço do usuário (via access_ok), e em seguida chama __copy_from_user e, por fim, __copy_from_user_ll. A partir daí, dependendo da arquitetura, uma chamada é feita para copiar do buffer do usuário para um buffer do kernel zerando (os bytes indisponíveis). As funções de assembly otimizadas incluem a capacidade de gerenciar.
A função strnlen_user
A função strnlen_user é usada da mesma maneira que strnlen, mas ela considera que o buffer está disponível no espaço do usuário. A função strnlen_user aceita dois parâmetros: o endereço do buffer no espaço do usuário e o comprimento máximo a ser verificado.
strnlen_user( src, n );
A função strnlen_user verifica primeiramente se o buffer do usuário é legível, por meio de uma chamada para access_ok. Se for acessível, a função strlen é chamada, e o argumento max length é ignorado.
A função strncpy_from_user
A função strncpy_from_user copia uma cadeia de caractere do espaço do usuário para um buffer do kernel, dados um endereço de origem no espaço do usuário e um comprimento máximo.
strncpy_from_user( dest, src, n );
Como se trata de uma cópia do espaço do usuário, a função primeiramente verifica se o buffer é gravável por meio de access_ok. De forma semelhante a copy_from_user, essa função é implementada como uma função assembly otimizada (em ./linux/arch/x86/lib/usercopy_XX.c).
Outros esquemas para mapeamento de memória
A seção anterior explorou métodos para movimentar dados entre o kernel e o espaço do usuário (com o kernel iniciando a operação). O Linux oferece outros métodos que podem ser usados para movimentação de dados, tanto no kernel como no espaço do usuário. Embora esses métodos possam não apresentar funcionalidade igual à das funções de acesso à memória do espaço do usuário, eles são semelhantes em sua capacidade de mapear memória entre os espaços de endereço.
Observe que, no espaço do usuário, como os processos do usuário aparecem em espaços de endereço separados, a movimentação de dados entre eles deve ocorrer por meio de algum tipo de mecanismo de comunicação interprocessos. O Linux oferece vários esquemas (tais como filas de mensagens), mas o mais notável é a memória compartilhada POSIX (shmem). Esse mecanismo permite que um processo crie uma área de memória e compartilhe essa região com um ou mais processos. Observe que cada processo pode mapear a região de memória compartilhada para diferentes endereços em seus respectivos espaços de endereços. Portanto é necessário um certo deslocamento de endereços.
A função mmap permite que um aplicativo do espaço do usuário crie um mapeamento no espaço de endereço virtual. Essa funcionalidade é comum em algumas classes de drivers de dispositivo (para desempenho), permitindo que a memória física do dispositivo seja mapeada para o espaço de endereço virtual do processo. Em um driver, a função mmap é implementada através da função do kernel remap_pfn_range, que fornece um mapeamento linear da memória do dispositivo para o espaço de endereço do usuário.
Conteúdo retirado do developerworks da IBM.
Observe que, no espaço do usuário, como os processos do usuário aparecem em espaços de endereço separados, a movimentação de dados entre eles deve ocorrer por meio de algum tipo de mecanismo de comunicação interprocessos. O Linux oferece vários esquemas (tais como filas de mensagens), mas o mais notável é a memória compartilhada POSIX (shmem). Esse mecanismo permite que um processo crie uma área de memória e compartilhe essa região com um ou mais processos. Observe que cada processo pode mapear a região de memória compartilhada para diferentes endereços em seus respectivos espaços de endereços. Portanto é necessário um certo deslocamento de endereços.
A função mmap permite que um aplicativo do espaço do usuário crie um mapeamento no espaço de endereço virtual. Essa funcionalidade é comum em algumas classes de drivers de dispositivo (para desempenho), permitindo que a memória física do dispositivo seja mapeada para o espaço de endereço virtual do processo. Em um driver, a função mmap é implementada através da função do kernel remap_pfn_range, que fornece um mapeamento linear da memória do dispositivo para o espaço de endereço do usuário.
Conteúdo retirado do developerworks da IBM.
Publicado no Fórum da Brutal Security por Natan