Memória: do endereço lógico ao físico

Resumo em uma linha

O programa fala em endereços lógicos, o hardware traduz pra físicos em tempo de execução, e é essa indireção que entrega proteção, relocação e a ilusão de que cada processo tem a memória inteira só pra si.

Você tem uma RAM física. Tem dezenas de processos. Cada um precisa de memória, cada um precisa estar protegido dos outros, e nenhum deles sabe — nem deveria saber — onde fisicamente acabou parando. Como o sistema operacional faz isso funcionar sem que um processo pise no outro?

Essa é a pergunta que organiza toda a gestão de memória. E a resposta começa com uma ideia simples e poderosa: separar o endereço que o programa enxerga do endereço que existe de verdade.

O problema: muitos processos, uma RAM

Volte ao 03 - Processos. Cada processo tem um address space: a faixa de endereços que ele acredita possuir — código, dados, heap, stack. O detalhe é que todos os processos acreditam na mesma coisa. Todos pensam que começam no endereço 0, que têm a memória toda disponível, que são os donos do lugar.

Isso é mentira — uma mentira útil, sustentada pelo sistema operacional e pelo hardware.

Por que não deixar cada programa usar endereços físicos diretos?

Porque aí o compilador precisaria saber, em tempo de compilação, exatamente onde o programa vai rodar na RAM. Mas você não sabe disso. Outro processo pode estar lá. A RAM pode ter tamanhos diferentes. E pior: nada impediria um programa de ler ou escrever na memória de outro. Sem indireção, não há proteção e não há flexibilidade.

A solução é interpor uma camada de tradução entre o que o programa diz e o que o hardware faz.

Endereço lógico × físico: a indireção que sustenta tudo

Dois mundos:

  • Endereço lógico (ou virtual) — o que a CPU gera quando o programa roda. É a visão do processo. Não existe fisicamente; é uma promessa.
  • Endereço físico — a posição real de um byte na RAM. É o que os fios de endereço da memória de fato selecionam.

Entre os dois fica a MMU (Memory Management Unit), uma peça de hardware que traduz cada acesso à memória, em tempo de execução, do lógico pro físico. O programa nunca toca no endereço físico; ele só conhece o lógico e confia na MMU pra entregar o byte certo.

A analogia do estacionamento

Imagine um estacionamento com manobrista. Você diz “quero meu carro de volta” e entrega o ticket número 7. Você não faz ideia de onde o carro está fisicamente — pode estar na vaga B-12, no subsolo, num andar que você nunca viu. O manobrista traduz “ticket 7” pro lugar físico real. O endereço lógico é o ticket; o físico é a vaga; a MMU é o manobrista. E o melhor: o manobrista pode mover seu carro de lugar sem você nem perceber, porque você só fala em ticket.

Esse diagrama mostra o caminho de cada acesso à memória:

flowchart LR
    CPU["CPU executa instrução<br/>gera endereço LÓGICO"] --> MMU["MMU<br/>(hardware de tradução)"]
    MMU -->|"traduz em<br/>tempo de execução"| FIS["endereço FÍSICO"]
    FIS --> RAM[("RAM física")]
    MMU -.->|"acesso inválido"| TRAP["trap → SO"]

Leitura do diagrama: a CPU nunca conversa direto com a RAM. Toda instrução que toca memória passa pela MMU. Se a tradução é válida, vira endereço físico e chega na RAM. Se é inválida — fora da faixa, sem permissão — a MMU dispara um trap e o controle volta pro sistema operacional.

Três presentes de uma única ideia

Essa indireção entrega três coisas de uma vez. Proteção: a MMU recusa acessos fora da faixa. Relocação: o SO pode mover o processo na RAM e só atualizar a tradução. Ilusão: cada processo enxerga um espaço contíguo começando em 0, mesmo espalhado fisicamente. Guarde isso — é a tese central de 07 - Memória virtual e paginação.

Relocação: o programa não sabe onde vai cair

Um programa compilado não sabe em que ponto da RAM será carregado. Como resolver os endereços?

Relocação estática — feita no load. Quando o programa é carregado, o carregador percorre o binário e soma o endereço-base real a cada referência. Depois disso, os endereços estão fixos: o programa não pode mais ser movido sem refazer tudo. Compactação fica impossível.

Relocação dinâmica — feita na execução, pelo hardware. O programa gera endereços lógicos a partir de 0; a MMU soma um registrador base em cada acesso. Mude o registrador base e o processo inteiro “anda” na RAM sem tocar uma linha do código. É essa que os sistemas modernos usam.

Por que dinâmica vence

Só com relocação dinâmica dá pra compactar a memória (juntar os processos pra fechar buracos) e pra swap mover um processo pra outra região. Estática trava tudo no lugar. Como dizem os textos: compactação só é possível se a relocação for em tempo de execução.

Base + limite: relocação e proteção no mesmo par

A relocação dinâmica resolve onde — mas precisamos também impedir que o processo acesse fora da sua faixa. A dupla clássica resolve os dois:

  • Registrador base — o endereço físico onde a faixa do processo começa. Soma-se a cada endereço lógico.
  • Registrador limite (bounds) — o tamanho da faixa. Todo endereço lógico é comparado: se passar do limite, trap.
flowchart TD
    LOG["endereço lógico<br/>(gerado pela CPU)"] --> CHK{"lógico &lt; limite?"}
    CHK -->|"não"| TRAP["TRAP → segfault<br/>SO mata o processo"]
    CHK -->|"sim"| ADD["físico = base + lógico"]
    BASE["registrador BASE"] --> ADD
    LIMIT["registrador LIMITE"] --> CHK
    ADD --> FIS["acesso permitido<br/>na RAM"]

Leitura do diagrama: a verificação de limite vem antes da soma. Primeiro o hardware confere se o endereço lógico cabe na faixa; só então soma a base pra achar o físico. Estourar o limite não vira “endereço de outro processo” — vira trap imediato.

A proteção é do HARDWARE, e isso não é detalhe

A comparação contra o limite acontece na MMU, em silício, a cada acesso. Software não dá conta: seria lento demais checar cada load/store em código, e um processo malicioso simplesmente ignoraria a checagem. Os registradores base e limite só podem ser alterados em modo privilegiado (kernel). Um processo de usuário não consegue mexer na própria faixa. É por isso que o SIGSEGV que mata seu programa quando você desreferencia ponteiro inválido nasce no hardware, não numa verificação amigável do compilador — liga em 03 - Processos (sinais).

Alocação contígua e o fantasma da fragmentação

A forma mais ingênua de dar memória: um bloco contíguo por processo. Cada um recebe uma faixa única e seguida, com seu par base+limite. Simples, rápido na tradução — e condenado a fragmentar.

Volte ao estacionamento. Se cada carro precisa de N vagas seguidas, e os carros chegam e saem o dia todo, em pouco tempo você tem vagas livres espalhadas que, somadas, dariam pra estacionar — mas nenhuma sequência contígua grande o suficiente. O carro novo não entra, mesmo havendo espaço total de sobra.

Dois fantasmas distintos:

  • Fragmentação externa — espaço livre entre os blocos, picotado em buracos pequenos. Há memória total suficiente, mas não num pedaço contíguo grande o bastante. O problema da alocação contígua.
  • Fragmentação interna — espaço desperdiçado dentro de um bloco já alocado, porque você deu ao processo mais do que ele pediu (arredondamento, blocos de tamanho fixo).
flowchart TD
    subgraph EXT["Fragmentação EXTERNA"]
        E1["P1: 4 KB"] --> E2["livre: 2 KB"] --> E3["P2: 6 KB"] --> E4["livre: 3 KB"] --> E5["P3: 5 KB"]
        E6["pedido: bloco contíguo de 4 KB<br/>total livre = 5 KB, mas picotado<br/>→ NÃO cabe"]
    end
    subgraph INT["Fragmentação INTERNA"]
        I1["bloco alocado: 8 KB"] --> I2["processo usa: 6 KB"] --> I3["desperdício interno: 2 KB"]
    end

Leitura do diagrama: na externa, somar os buracos (2 + 3 = 5 KB) não ajuda — o pedido exige contiguidade. Na interna, o desperdício mora dentro do que já foi entregue: o processo recebeu mais do que usa.

Estratégias de encaixe — e por que nenhuma salva

Quando chega um pedido, qual buraco usar?

EstratégiaEscolheCusto
First-fito primeiro buraco que serverápido; fragmenta o começo da lista
Best-fito menor buraco que servedeixa sobras minúsculas e inúteis
Worst-fito maior buracotenta deixar sobras aproveitáveis; varre tudo

Nenhuma resolve, todas mitigam

First-fit costuma ser o melhor na prática (rápido e razoável), mas toda estratégia de alocação contígua adia a fragmentação externa em vez de eliminá-la. A saída drástica é a compactação: mover processos na RAM pra juntar os buracos num bloco grande. Funciona — mas é cara (copiar memória, parar processos) e só é possível com relocação dinâmica. Você não quer compactar a RAM toda hora.

É essa parede que motiva a próxima virada.

Segmentação: dividir por significado, não por bloco único

Em vez de um bloco contíguo só, e se dividíssemos o espaço do processo em segmentos lógicos — um pra código, um pros dados, um pra stack? Cada segmento ganha seu próprio par base+limite e pode morar em qualquer lugar da RAM.

Isso é mais natural: o programa pensa nessas partes separadas. O stack cresce pra um lado, o heap pra outro, o código é só leitura. Segmentação espelha essa estrutura em vez de empacotar tudo num bloco rígido.

Um endereço lógico passa a ser um par: (número do segmento, deslocamento). O hardware usa o número pra achar a entrada na tabela de segmentos, que guarda base e limite daquele segmento.

flowchart TD
    LOG["endereço lógico<br/>(segmento, deslocamento)"] --> SEG{"segmento"}
    SEG --> TAB["Tabela de Segmentos"]
    TAB --> B["base do segmento"]
    TAB --> L["limite do segmento"]
    LOG --> OFF["deslocamento"]
    OFF --> CHK{"deslocamento &lt; limite?"}
    L --> CHK
    CHK -->|"não"| TRAP["TRAP<br/>(violação de segmento)"]
    CHK -->|"sim"| ADD["físico = base + deslocamento"]
    B --> ADD
    ADD --> RAM[("RAM")]

Leitura do diagrama: cada segmento repete a lógica de base+limite, só que indexada por uma tabela. O deslocamento é checado contra o limite daquele segmento — não do processo inteiro. Estourar o tamanho de um segmento dispara o trap de violação.

Segmentação não tem fragmentação interna — mas herda a externa

Como cada segmento é alocado com o tamanho exato que precisa (partições dinâmicas, encaixe justo), não há desperdício interno. Mas os segmentos ainda são blocos contíguos de tamanhos variados espalhados pela RAM — então a fragmentação externa volta. Segmentação melhora a organização e a proteção granular (o segmento de código pode ser read-only), mas não mata o problema de fundo: pedaços de tamanho variável sempre deixam buracos.

A virada pra paginação

Repare no padrão. Bloco único: fragmentação externa. Segmentação: ainda fragmentação externa. A raiz é sempre a mesma — alocar pedaços de tamanho variável. Encontrar um buraco contíguo do tamanho certo é o problema.

E se eliminássemos o “tamanho certo”?

A paginação divide a memória em páginas de tamanho fixo (tipicamente 4 KB). Como todo pedaço tem o mesmo tamanho, qualquer frame livre serve pra qualquer página. Não existe mais “buraco grande o bastante” — todo buraco tem exatamente um tamanho. A fragmentação externa desaparece (sobra só fragmentação interna na última página, pequena e limitada).

É aqui que esta nota entrega o bastão

A paginação é a resposta madura ao problema que esta nota inteira construiu: indireção (lógico × físico) + tamanho fixo (mata a fragmentação externa) = a base da memória virtual moderna. O como — tabelas de páginas, TLB, page faults — é o assunto de 07 - Memória virtual e paginação, e a pressão que ela sofre quando a RAM aperta é 08 - Substituição de páginas e thrashing.

flowchart LR
    PROB["1 RAM,<br/>N processos"] --> IND["indireção<br/>lógico × físico (MMU)"]
    IND --> CONT["alocação contígua<br/>base + limite"]
    CONT -->|"fragmentação externa"| SEG["segmentação<br/>(segmentos lógicos)"]
    SEG -->|"ainda fragmenta:<br/>tamanho variável"| PAG["PAGINAÇÃO<br/>tamanho fixo → frame livre serve"]
    PAG --> VM["memória virtual<br/>(nota 07)"]

Leitura do diagrama: a história inteira da gestão de memória é uma escada de soluções, cada degrau corrigindo a limitação do anterior — até a paginação fixar o tamanho e dissolver a fragmentação externa.

O tamanho do espaço de endereço: 32 bits × 64 bits

Até aqui falamos de como traduzir. Falta a pergunta de tamanho: quantos endereços lógicos diferentes o programa pode gerar? Isso é o tamanho do address space, e ele depende de quantos bits a CPU usa pra endereçar.

Com 32 bits, você endereça 2³² bytes = 4 GB. Esse foi o teto histórico de uma era inteira de software. Cada processo enxergava no máximo 4 GB de espaço virtual, e a soma de RAM física que o sistema conseguia usar batia no mesmo limite. O remendo da época foi o PAE (Physical Address Extension): um truque que ampliava o endereço físico pra além de 4 GB sem mexer no espaço lógico de 32 bits de cada processo. Ou seja, a máquina podia ter 16 GB de RAM, mas nenhum processo individual via mais que ~4 GB. PAE era exatamente isso: um remendo, não uma cura.

Com 64 bits, o espaço explode. 2⁶⁴ é um número absurdo — 16 exabytes. Mas nenhum hardware atual implementa os 64 bits inteiros: seria desperdício de silício em tabelas de tradução para um espaço que ninguém preenche. O x86-64 implementa 48 bits canônicos (2⁴⁸ = 256 TiB), divididos em duas metades — endereços baixos pra user space, endereços altos pro kernel — com um buraco não-canônico no meio. Um endereço só é válido (canonical) se os bits 48 a 63 replicam o bit 47; pular pra dentro do buraco dispara General Protection Fault. CPUs mais novas estendem isso pra 57 bits (tabelas de página de cinco níveis), chegando a 128 PiB.

flowchart TD
    subgraph B32["32 bits → 2³² = 4 GB"]
        T32["espaço virtual por processo: ~4 GB<br/>PAE: estende só o FÍSICO (remendo)"]
    end
    subgraph B64["64 bits → 48 canônicos = 256 TiB"]
        LOW["baixo: 0x0000... → user space"]
        HOLE["buraco NÃO-CANÔNICO<br/>(bits 48-63 ≠ bit 47 → GP fault)"]
        HIGH["alto: 0xFFFF... → kernel"]
        LOW --> HOLE --> HIGH
    end
    B32 -->|"a régua aumentou"| B64

Leitura do diagrama: 32 bits dão uma régua de 4 GB, e PAE só estica o lado físico. 64 bits não usam todos os bits — o x86-64 corta em 48 canônicos, partindo o espaço em metade baixa (processo) e metade alta (kernel), com um abismo proibido entre elas.

Por que uma régua gigante muda o jogo

Quando o espaço lógico é praticamente infinito comparado à RAM, o SO pode ser generoso. Dois efeitos concretos. Overcommit: o kernel promete mais memória do que existe fisicamente, apostando que ninguém usa tudo de uma vez (volta em 07 - Memória virtual e paginação). mmap folgado: mapear um arquivo de 10 GB no espaço de um processo é trivial quando você tem 256 TiB de endereços pra gastar — você reserva faixas enormes sem custo físico, porque endereço lógico não custa RAM até ser tocado. A indireção lógico×físico só fica realmente poderosa quando o lado lógico é vasto.

Segmentação + paginação: como o x86 juntou os dois

A nota tratou segmentação e paginação como degraus alternativos. Na história real do x86, eles foram empilhados. O processador clássico (modo protegido de 32 bits) fazia duas traduções em sequência:

  1. Segmentação transforma o endereço lógico (seletor de segmento, deslocamento) num endereço linear, somando a base do segmento.
  2. Paginação transforma esse endereço linear no endereço físico, via tabela de páginas.
flowchart LR
    LOG["lógico<br/>(segmento : deslocamento)"] --> SEG["SEGMENTAÇÃO<br/>+ base do segmento"]
    SEG --> LIN["endereço LINEAR"]
    LIN --> PAG["PAGINAÇÃO<br/>tabela de páginas"]
    PAG --> FIS["endereço FÍSICO"]
    FIS --> RAM[("RAM")]

Leitura do diagrama: dois estágios em fila. O segmento produz um endereço linear; só então a paginação o aterrissa num endereço físico. Eram duas camadas de indireção, uma sobre a outra.

E por que sumiu? No x86-64, a segmentação foi quase toda desligada. No modo de 64 bits, os segmentos de código, dados e stack (CS, DS, SS, ES) têm base fixa em zero e limite ignorado — é o modelo de memória plano (flat memory model): o endereço lógico já é o linear, sem soma de base, sem etapa de segmento. Só sobraram resquícios. FS e GS ainda carregam uma base configurável (via MSRs) e são usados pra coisas como thread-local storage (glibc usa FS em user space) e variáveis por-CPU no kernel (GS). Fora isso, segmentação virou vestígio: os SOs modernos confiam só na paginação pra proteção e tradução.

Por que a paginação venceu a segmentação

Segmentos são blocos de tamanho variável — e já vimos aonde isso leva: fragmentação externa. Páginas de tamanho fixo não têm esse problema. Some a isso a portabilidade (paginação não amarra o SO ao esquema de segmentos do x86) e fica claro por que Linux e Windows escolheram o modelo plano. A segmentação resolvia proteção e organização, mas a paginação resolve isso mais a fragmentação — então a camada extra de segmento virou peso morto.

Como o kernel aloca memória física

A MMU traduz, mas alguém precisa decidir qual frame físico entregar. Esse é o trabalho do alocador de memória física do kernel, e ele tem dois andares.

Buddy allocator — o andar de baixo. Gerencia os frames físicos em blocos de potências de 2 de páginas (um frame de 4 KB é ordem 0; 8 KB é ordem 1; e assim por diante). Quando você pede um bloco e só existe um maior livre, ele divide pela metade repetidamente até chegar no tamanho certo — as duas metades são “buddies”. Quando um bloco é liberado e seu buddy também está livre, eles se fundem de volta num bloco maior. Esse jogo de dividir e fundir combate a fragmentação externa de frames: o problema da alocação contígua não desaparece, ele é domado mantendo blocos contíguos organizados por tamanho.

flowchart TD
    O3["bloco ordem 3: 8 páginas (32 KB)"] --> S1["divide → 2 × ordem 2 (16 KB)"]
    S1 --> A2["ordem 2: ALOCADO"]
    S1 --> B2["ordem 2: livre"]
    B2 --> S2["pedido de 1 página → divide ordem 2 → ordem 1 → ordem 0"]
    S2 --> A0["ordem 0: ENTREGUE (4 KB)"]
    S2 --> BUD["buddies livres aguardam fusão"]

Leitura do diagrama: o buddy parte de um bloco grande e o quebra ao meio sucessivamente até o tamanho pedido. As metades não usadas viram buddies livres; quando o vizinho também liberar, fundem-se de novo. Tudo em potências de 2, o que torna divisão e fusão baratas (só bit-shift e XOR de endereço).

Slab allocator — o andar de cima. O buddy entrega no mínimo uma página inteira (4 KB), mas o kernel vive criando objetos pequenos: descritores de inode, entradas de tabela de processo, estruturas de rede — uns poucos bytes ou dezenas de bytes cada. Pedir uma página de 4 KB pra um objeto de 64 bytes seria desperdício grosseiro (fragmentação interna). O slab pega páginas do buddy e as fatia em objetos de tamanho fixo, mantendo caches por tipo de objeto. Aloca e libera objeto vira quase instantâneo, e os objetos já vêm “pré-formatados”. É a divisão de trabalho clássica: buddy para blocos grandes e contíguos, slab para os miúdos frequentes.

ASLR: a indireção vira defesa

Há um bônus de segurança escondido na separação lógico×físico. Se o atacante quer sequestrar a execução de um programa — sobrescrever um endereço de retorno, redirecionar pra um gadget —, ele precisa saber onde as coisas estão no espaço de endereço: onde começa a stack, onde está carregada a libc, onde fica o executável. Num layout fixo e previsível, esses endereços são os mesmos em toda execução, e o exploit é confiável.

O ASLR (Address Space Layout Randomization) quebra essa previsibilidade: a cada execução, o kernel e o loader sorteiam posições diferentes pra stack, heap, bibliotecas e — se o binário for compilado como PIE (Position Independent Executable) — pro próprio executável. O atacante deixa de saber pra onde apontar; o que funcionava numa máquina falha na próxima, e o exploit vira um chute. É a indireção lógico×físico colhida como defesa: como o programa só fala em endereços lógicos e nada o amarra a posições fixas, o SO tem liberdade de embaralhar o layout sem que o código perceba. ASLR não é bala de prata (vazar um único ponteiro pode derrubá-lo, e o kASLR do kernel é mais frágil), e segurança a fundo é assunto de um galho futuro — mas o gancho conceitual é este: a mesma camada que dá relocação dá randomização.

Por que alocação contígua ainda aparece

A paginação aposentou a alocação contígua para processos. Mas o problema não morreu — mudou de lugar. Tem um cliente que ainda exige memória física contígua: o DMA (Direct Memory Access).

Um dispositivo que faz DMA escreve direto na RAM sem passar pela CPU — e muitos deles (GPUs, controladoras de vídeo, hardware sem IOMMU) não enxergam a tabela de páginas do SO. Pra eles, uma faixa de memória “contígua” em endereços lógicos pode estar espalhada em frames físicos picotados, e o dispositivo escreveria no lugar errado. Esse hardware precisa de um bloco fisicamente contíguo de verdade. A tradução lógico×físico, que liberta os processos, não ajuda quem ignora a MMU — liga em 10 - I-O e o subsistema de entrada e saída (DMA).

CMA: a fragmentação externa reaparece, e o kernel tem resposta

Conseguir um bloco físico grande e contíguo num sistema que roda há semanas é exatamente o velho problema da fragmentação externa, agora no nível dos frames. A solução do Linux é o CMA (Contiguous Memory Allocator): reserva uma região no boot que normalmente só aceita páginas movíveis; quando um driver precisa do bloco contíguo, o kernel migra essas páginas pra fora e entrega a área limpa. O fantasma do começo da nota — “preciso de N vagas seguidas” — nunca foi exorcizado de vez. A paginação o empurrou pra borda do sistema, onde o hardware de I/O insiste em falar a língua dos endereços físicos.

Em entrevista

A few sentences worth having ready in English.

Memory management gives every process the illusion of owning a private, contiguous address space starting at zero — the program emits logical addresses, and the MMU translates them to physical addresses on every access at runtime. This indirection is what buys you protection, relocation, and the per-process illusion all at once. Dynamic relocation uses a base register the hardware adds to each address, while a limit register bounds the access — go past the limit and the MMU raises a trap, which is exactly where a segfault comes from. Contiguous allocation suffers from external fragmentation: enough total free memory but no single contiguous hole big enough; segmentation splits the space into logical segments (code, data, stack) with per-segment base and limit, which removes internal fragmentation but still leaves external fragmentation because segments are variable-sized. The key insight to land is that paging fixes the size of every chunk, so any free frame fits any page and external fragmentation disappears — which is why modern systems page instead of segment. If pushed on why protection lives in hardware, the answer is speed and trust: checking every load and store in software would be too slow and a malicious process could just skip the check. On address-space size, 32-bit gave each process about 4 GB and PAE only patched the physical side; 64-bit hardware doesn’t wire up all 64 bits — x86-64 uses 48 canonical bits (256 TiB) split into a low half for user space and a high half for the kernel, with a non-canonical hole in between. That huge logical space is what makes overcommit and generous mmap cheap. Segmentation on x86-64 is essentially obsolete — the flat model fixes segment bases at zero and only FS/GS survive for thread-local and per-CPU data — so paging carries all the protection and translation. Underneath, the kernel hands out physical frames with a buddy allocator (power-of-two blocks, split and merged to fight external fragmentation of frames) and a slab allocator on top for small, frequent kernel objects. And the same logical/physical indirection that buys relocation also buys ASLR: randomizing the layout each run breaks the attacker’s assumption about where the stack, heap, and libraries live.

Vocabulário

PTEN
endereço lógico / virtuallogical / virtual address
endereço físicophysical address
unidade de gerenciamento de memóriamemory management unit (MMU)
relocaçãorelocation
registrador base / limitebase / limit register
fragmentação internainternal fragmentation
fragmentação externaexternal fragmentation
segmentaçãosegmentation
compactaçãocompaction
tradução de endereçoaddress translation
espaço de endereço canônicocanonical address space
modelo de memória planoflat memory model
alocador buddy / slabbuddy / slab allocator
randomização do layout do espaço de endereçoaddress space layout randomization (ASLR)
memória contígua / DMAcontiguous memory / DMA

Lastro

Veja também