Como o kernel sabe o endereço básico da memória física?
Estou tentando entender 2 questões intimamente relacionadas.
O código do kernel que executa pós-bootloader e antes de habilitar o MMU opera em memória virtual física / com identidade mapeada. Como esse código é tornado portátil entre diferentes CPUs que podem ter DRAM em diferentes intervalos de endereços físicos?
Para que o kernel gerencie a tabela de páginas, ele precisa estar ciente de quais recursos de memória física estão disponíveis, incluindo o endereço de base de memória física e a memória física disponível, de forma que não atribua endereços físicos fora do intervalo DRAM.
Eu imagino que isso seja um tanto dependente da implementação, mas referências a como diferentes arquiteturas lidam com esse problema seriam apreciadas. Algumas ideias que tenho até agora:
O intervalo de DRAM do endereço físico, ou pelo menos o endereço base, é incluído no tempo de compilação do kernel. Isso implica que a recompilação é necessária para diferentes CPUs, mesmo com o mesmo ISA. Isso é inspirado por esta resposta aqui , que, se estou entendendo corretamente, descreve a mesma solução para o endereço base do kernel. Como o endereço de base é conhecido em tempo de compilação, o código do kernel faz referência a endereços literais em vez de deslocamentos do endereço de base de DRAM / kernel.
As informações de DRAM são lidas e aprendidas da árvore de dispositivos com o restante do mapa de memória física. Esta é minha impressão pelo menos para os SoC da Xilinx Zynq, com base em postagens de fórum como esta . Embora esta solução ofereça mais flexibilidade e nos permita apenas recompilar o carregador de boot ao invés de todo o kernel para portar CPU, isso me deixa pensando como minha máquina pessoal X86 pode detectar em tempo de execução quanta DRAM eu instalei. O código para gerenciar a tabela de páginas apenas faz referência a deslocamentos do endereço de base DRAM e é portátil sem recompilação entre CPUs com diferentes intervalos de endereços físicos de DRAM.
Respostas
Todos os DIMMs de memória física que estão disponíveis no momento da inicialização podem não e normalmente não são mapeados para um único intervalo contíguo do espaço de endereço da memória física, portanto, não há um "endereço básico". Em uma reinicialização a frio, após a conclusão da execução do firmware da CPU, o firmware da plataforma, que normalmente é BIOS legado ou UEFI, é executado. Uma determinada placa-mãe é compatível apenas com um conjunto limitado de coleções de CPU que normalmente têm o mesmo método para descobrir memória física, incluindo DIMMs e o dispositivo de memória do firmware da plataforma. Uma implementação do firmware de plataforma usa este método para construir uma tabela de entradas de descrição de memória onde cada entrada descreve um intervalo de endereço de memória física. Para obter mais informações sobre a aparência deste processador, consulte: Como o BIOS inicializa a DRAM? . Esta tabela é armazenada em um endereço na memória principal (DIMMs) que é conhecido por ser reservado para essa finalidade e deve ser apoiado pela memória real (um sistema pode ser inicializado sem quaisquer DIMMs).
A maioria das implementações do BIOS do PC x86 desde meados dos anos 90 oferece a INT 15h E820h
função de modo real (15h é o número de interrupção e E820h é um argumento passado no AX
registro). Esta é uma função BIOS específica do fornecedor introduzida pela primeira vez no PhoenixBIOS v4.0 (1992-1994, não consigo definir o ano exato) e posteriormente adotada por outros fornecedores BIOS. Essa interface foi estendida pela especificação ACPI 1.0 lançada em 1996 e revisões posteriores do PhoenixBIOS com suporte para ACPI. A interface UEFI correspondente é GetMemoryMap()
, que é um serviço de inicialização UEFI (o que significa que só pode ser chamada durante a inicialização, conforme definido na especificação UEFI). O kernel pode usar uma dessas interfaces para obter o mapa de endereços que descreve a memória em todos os nós NUMA. Outros métodos (mais antigos) em plataformas x86 são discutidos em Detecção de memória (x86) . Ambas as especificações ACPI começando com a versão? e a especificação UEFI começando com a versão? suporta tipos de intervalo de memória DRAM DIMMs e NVDIMMs.
Considere, por exemplo, como o kernel do Linux compatível com ACPI determina quais intervalos de endereços físicos estão disponíveis (ou seja, apoiados pela memória real) e utilizáveis (ou seja, gratuitos) em uma plataforma BIOS compatível com ACPI x86. O firmware do BIOS carrega o carregador de inicialização do dispositivo de armazenamento inicializável especificado para um local de memória dedicado para esse propósito. Depois que o firmware completa a execução, ele salta para o carregador de inicialização, que encontrará a imagem do kernel na mídia de armazenamento, carrega-a na memória e transfere o controle para o kernel. O próprio bootloader precisa saber o mapa de memória atual e alocar um pouco de memória para sua operação. Ele tenta obter o mapa de memória chamando a E820h
função e, se não houver suporte, recorrerá a interfaces BIOS de PC mais antigas. O protocolo de inicialização do kernel define quais intervalos de memória podem ser usados pelo carregador de inicialização e quais intervalos de memória devem ser deixados disponíveis para o kernel.
O bootloader em si não modifica o mapa de memória ou fornece o mapa para o kernel. Em vez disso, quando o kernel começa a ser executado, ele chama a E820h
função e passa para ela um ponteiro de 20 bits (in ES:DI
) para um buffer que o kernel sabe que está livre em plataformas x86 de acordo com o protocolo de inicialização. Cada chamada retorna um descritor de intervalo de memória cujo tamanho é de pelo menos 20 bytes. Para obter mais informações, consulte a versão mais recente da especificação ACPI. A maioria das implementações de BIOS oferece suporte a ACPI.
Assumindo um kernel Linux com parâmetros de inicialização padrão do upstream, você pode usar o comando dmesg | grep 'BIOS-provided\|e820'
para ver a tabela do descritor de intervalo de memória retornada. No meu sistema, é assim:
[ 0.000000] BIOS-provided physical RAM map:
[ 0.000000] BIOS-e820: [mem 0x0000000000000000-0x00000000000917ff] usable
[ 0.000000] BIOS-e820: [mem 0x0000000000091800-0x000000000009ffff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000000e0000-0x00000000000fffff] reserved
[ 0.000000] BIOS-e820: [mem 0x0000000000100000-0x00000000d2982fff] usable
[ 0.000000] BIOS-e820: [mem 0x00000000d2983000-0x00000000d2989fff] ACPI NVS
[ 0.000000] BIOS-e820: [mem 0x00000000d298a000-0x00000000d2db9fff] usable
[ 0.000000] BIOS-e820: [mem 0x00000000d2dba000-0x00000000d323cfff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000d323d000-0x00000000d7eeafff] usable
[ 0.000000] BIOS-e820: [mem 0x00000000d7eeb000-0x00000000d7ffffff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000d8000000-0x00000000d875ffff] usable
[ 0.000000] BIOS-e820: [mem 0x00000000d8760000-0x00000000d87fffff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000d8800000-0x00000000d8fadfff] usable
[ 0.000000] BIOS-e820: [mem 0x00000000d8fae000-0x00000000d8ffffff] ACPI data
[ 0.000000] BIOS-e820: [mem 0x00000000d9000000-0x00000000da718fff] usable
[ 0.000000] BIOS-e820: [mem 0x00000000da719000-0x00000000da7fffff] ACPI NVS
[ 0.000000] BIOS-e820: [mem 0x00000000da800000-0x00000000dbe11fff] usable
[ 0.000000] BIOS-e820: [mem 0x00000000dbe12000-0x00000000dbffffff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000dd000000-0x00000000df1fffff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000f8000000-0x00000000fbffffff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000fec00000-0x00000000fec00fff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000fed00000-0x00000000fed03fff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000fed1c000-0x00000000fed1ffff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000fee00000-0x00000000fee00fff] reserved
[ 0.000000] BIOS-e820: [mem 0x00000000ff000000-0x00000000ffffffff] reserved
[ 0.000000] BIOS-e820: [mem 0x0000000100000000-0x000000041edfffff] usable
[ 0.002320] e820: update [mem 0x00000000-0x00000fff] usable ==> reserved
[ 0.002321] e820: remove [mem 0x000a0000-0x000fffff] usable
[ 0.002937] e820: update [mem 0xdd000000-0xffffffff] usable ==> reserved
[ 0.169287] e820: reserve RAM buffer [mem 0x00091800-0x0009ffff]
[ 0.169288] e820: reserve RAM buffer [mem 0xd2983000-0xd3ffffff]
[ 0.169289] e820: reserve RAM buffer [mem 0xd2dba000-0xd3ffffff]
[ 0.169289] e820: reserve RAM buffer [mem 0xd7eeb000-0xd7ffffff]
[ 0.169289] e820: reserve RAM buffer [mem 0xd8760000-0xdbffffff]
[ 0.169290] e820: reserve RAM buffer [mem 0xd8fae000-0xdbffffff]
[ 0.169291] e820: reserve RAM buffer [mem 0xda719000-0xdbffffff]
[ 0.169291] e820: reserve RAM buffer [mem 0xdbe12000-0xdbffffff]
[ 0.169292] e820: reserve RAM buffer [mem 0x41ee00000-0x41fffffff]
Os intervalos de memória que começam com "BIOS-e820" são descritos nessa tabela. A primeira linha indica claramente a origem dessas informações. O formato exato dessas informações depende da versão do kernel Linux. Em qualquer caso, você verá um intervalo e um tipo em cada entrada. As linhas que começam com "e820" (sem a parte "BIOS-") são alterações que o próprio kernel fez na tabela. A implementação do E820h
pode ser problemática ou pode haver sobreposições entre os intervalos obtidos em entradas diferentes. O kernel executa as verificações e mudanças necessárias de acordo. Os intervalos marcados como "utilizáveis" são em sua maioria livres para uso pelo kernel com exceções discutidas na especificação ACPI e das quais o kernel está ciente. A grande maioria das implementações de BIOS do PC retorna no máximo 128 descritores de intervalos de memória. Versões mais antigas do kernel Linux só podiam lidar com até 128 intervalos de memória, portanto, quaisquer entradas retornadas E820h
além do 128º são ignoradas. A partir da versão?, Essa limitação foi relaxada. Para obter mais informações, consulte a série de patches do kernel intitulada "x86 boot: passe entradas do mapa de memória E820 mais de 128 por meio de uma lista vinculada de dados de configuração."
Intervalos de tipo usable
e ACPI data
. Faixas de tipo reserved
são apoiadas por DRAM DIMMs ou cortadas para MMIO pela CPU ou firmware da plataforma. Faixas de tipo ACPI NVS
são apoiadas pela memória do firmware. Todas as outras faixas não são recuperadas pela memória real, tanto quanto o firmware pode dizer. Observe que o firmware pode optar por não mapear todos os DRAM DIMMs ou NVDIMMs instalados. Isso pode acontecer se a configuração da memória física não for suportada como está ou se o firmware não conseguir obter informações de um DIMM instalado devido a um problema no DIMM.
Você pode calcular quanta memória dos DRAM DIMMs e NVDIMMs instalados é disponibilizada pelo firmware para o kernel. No meu sistema, instalei 16 GBs de DRAM DIMMs. Portanto, a menos que alguns dos DIMMs não estejam instalados corretamente, não funcionem corretamente, seja um bug no firmware ou não sejam suportados pela plataforma ou processador, deve haver um pouco menos de 16 GBs disponíveis para o kernel.
Todos os usable
intervalos somam bytes 0x3FA42B800. Observe que o último endereço de um intervalo é inclusivo, o que significa que aponta para uma localização de byte que faz parte do intervalo. A quantidade total de DIMMs fisicamente instalados é de 16 GBs ou 0x400000000 bytes. Portanto, a quantidade total de memória instalada que não foi disponibilizada para o kernel é 0x400000000 - 0x3FA42B800 ou cerca de 92 MBs do total de 16 GBs. Esta memória foi levada por alguns dos reserved
intervalos e todos os ACPI data
intervalos. Se determinados locais em um DRAM DIMM ou NVDIMM foram determinados pelo firmware da plataforma como não confiáveis, eles também serão cortados como reserved
.
Observe que o intervalo 0x000a0000-0x000fffff não é descrito no E820
mapa de memória de acordo com a especificação ACPI. Esta é a área de memória superior de 640 KB-1 MB. O kernel imprime uma mensagem que diz que removeu este intervalo da área de memória utilizável para manter a compatibilidade com sistemas antigos.
Neste ponto, a memória a ser usada como MMIO para a maioria dos dispositivos PCIe ainda não foi alocada. Meu processador suporta um espaço de endereço físico de 39 bits, o que significa que endereços entre 0 a 2 ^ 39 estão disponíveis para mapeamento. Até agora, apenas a maioria dos 16,5 GBs inferiores desse espaço foi mapeada para algo. Observe que ainda existem lacunas não mapeadas neste intervalo. O kernel pode usar essas lacunas (alguns 100s de MBs) e o resto do espaço de endereço físico (cerca de 495,5 GBs) para alocar intervalos de endereços para dispositivos IO. O kernel eventualmente descobrirá dispositivos PCIe e para cada dispositivo, ele tentará carregar um driver compatível, se disponível. O driver então determina quanta memória o dispositivo precisa e quaisquer restrições nos endereços de memória impostas pelo dispositivo e solicita do kernel para alocar memória para o dispositivo e configurá-lo como uma memória MMIO pertencente ao dispositivo. Você pode ver o mapa de memória final usando o comando sudo cat /proc/iomem
.
Existem situações em que você deseja alterar manualmente o tipo de memória de um intervalo de memória existente (por exemplo, para teste), criar um novo intervalo (por exemplo, para emular memória persistente em DRAM ou se o firmware não conseguir descobrir todos os memória disponível por qualquer motivo), reduza a quantidade de memória utilizável pelo kernel (por exemplo, para evitar que um hipervisor bare-metal use memória além de um limite e torne o resto disponível para convidados), ou até mesmo substituir completamente toda a tabela retornada de E820h
. Os parâmetros mem
e do memmap
kernel podem ser usados para tais propósitos. Quando um ou mais desses parâmetros são especificados com valores válidos, o kernel primeiro lê o mapa de memória fornecido pelo BIOS e faz as alterações de acordo. O kernel imprimirá o mapa de memória final como "mapa de RAM físico definido pelo usuário". no buffer de anel de mensagem do kernel. Você pode ver essas mensagens com dmesg | grep user:
(cada linha de intervalo de memória começa com "usuário:"). Essas mensagens serão impressas após as mensagens "BIOS-e820".
Em uma plataforma x86 inicializada com firmware UEFI que suporta o Módulo de Suporte de Compatibilidade (consulte a especificação CSM para obter mais informações, que é separada da UEFI), a E820h
interface de modo real legado é suportada e o kernel Linux por padrão ainda a usa. Se o kernerl estiver sendo executado em uma plataforma x86 com UEFI que não oferece suporte a CSM, a E820h
interface pode não fornecer todos ou nenhum intervalo de memória. Pode ser necessário usar o add_efi_memmap
parâmetro kernel em tais plataformas. Um exemplo pode ser encontrado em UEFI Memory V E820 Memory . Quando um ou mais intervalos de memória são fornecidos de GetMemoryMap()
, o kernel mescla esses intervalos com aqueles da E820h
interface. O mapa de memória resultante pode ser visualizado usando dmesg | grep 'efi:'
Outro parâmetro de kernel relacionado à UEFI que afeta o mapa de memória é efi_fake_mem
.
A especificação ACPI (Seção 6.3) fornece mecanismos de notificação para informar o kernel quando um dispositivo IO ou DIMM foi inserido ou removido do sistema em qualquer estado S. (Não sei se há algum motherboads que suporta a remoção de DIMMs em qualquer estado S, no entanto. Isso geralmente só é possível no estado G3 e talvez S4 e / ou S5) Quando tal evento ocorre, o kernel ou o firmware faz alterações no mapa de memória de acordo. Essas mudanças são refletidas em sudo cat /proc/iomem
.
O endereçamento relativo ao pc refere-se a uma técnica de programação em que seu programa pode operar em qualquer endereço. Uma vez que os registros de relocação (por exemplo, segmentos) se tornaram obsoletos, a maior parte da programação relativa ao PC é executada explicitamente. Aqui está um exemplo em um tipo genérico de código de máquina:
.text
entry:
call reloc /* call is pc relative */
reloc:
pop %r0 /* r0 now contains physical address of reloc */
sub $reloc, %r0, %r14 /* r14 contains difference between link address of reloc */ /* At this point, r14 is a relocation register. A virtual address + r14 == the corresponding physical address. */ add $proot, %r14, %r0 /* physical address of page table root */
add $entry, %r14, %r1 /* entry is where we were loaded into ram */ test $0xfff, %r1 /* someone is being funny and not page aligning us */
jnz bad_alignment
or $0x7, %r1 /* put mythical page protection bits in r1 */ mov $1024, %r2 /* number of pages in r2 */
loop:
store %r1, (%r0) /* store a page table entry */
add $0x1000, %r1 /* setup next one 4096 bytes farther */ add $4, %r0 /* point to next page table entry */
sub $1, r2 /* are we done? */ cmp %0, r2 jne loop /* nope, setup next entry */ add $proot, %r14, %r0
loadsysreg %r0, page_table_base_register
mov $1, %r0 mov $v_entry, %r1
loadsysreg %r0, page_table_enabled
jmp %r1
v_entry:
/* now we are virtually addressed */
call main
1: jmp 1b /* main shouldn't return. */
.data
.align 12 /* 4096 byte pages */
proot:
.zero 4096
.text
Essa máquina mítica é muito simples, com uma única tabela de página plana, e o kernel está vinculado no endereço 0, mas pode ser executado de qualquer lugar nos primeiros 4M (1024 * 4096). As máquinas reais são apenas versões mais detalhadas disso. Em geral, você não pode confiar nem mesmo nas linguagens do sistema, como C
até ter a configuração inicial do espaço de endereço. Assim que estiver, o código pode construir tabelas de páginas muito mais intrincadas e consultar bancos de dados como a árvore de dispositivos ou até mesmo monstruosidades como apic / uefi para obter mais informações sobre o layout da ram, etc.
Em arquiteturas de tabela de página mapeada para frente, onde os nós internos estão em um formato compatível como os nós folha (x86-classic, por exemplo), você pode usar uma tabela de página única recursivamente para permitir um endereço de link mais flexível. Por exemplo, se você apontou a última entrada em proot (que é proot [1023]) de volta para proot, então você poderia vincular seu sistema operacional em 0xffffc000, e este código funcionaria (uma vez traduzido para x86).