559 lines
22 KiB
Python
Executable File
559 lines
22 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
#
|
|
# Copyright (c) 2019 Intel Corporation
|
|
#
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
"""Generate MMU page tables for x86 CPUs.
|
|
|
|
This script generates 64-bit PAE style MMU page tables for x86.
|
|
Even though x86 is a 32-bit target, we use this type of page table
|
|
to support the No-Execute (NX) bit. Please consult the IA
|
|
Architecture SW Developer Manual, volume 3, chapter 4 for more
|
|
details on this data structure.
|
|
|
|
The script takes as input the zephyr_prebuilt.elf kernel binary,
|
|
which is a link of the Zephyr kernel without various build-time
|
|
generated data structures (such as the MMU tables) inserted into it.
|
|
The build cannot easily predict how large these tables will be,
|
|
so it is important that these MMU tables be inserted at the very
|
|
end of memory.
|
|
|
|
Of particular interest is the "mmulist" section, which is a
|
|
table of memory region access policies set in code by instances
|
|
of MMU_BOOT_REGION() macros. The set of regions defined here
|
|
specifies the boot-time configuration of the page tables.
|
|
|
|
The output of this script is a linked set of page tables, page
|
|
directories, and a page directory pointer table, which gets linked
|
|
into the final Zephyr binary, reflecting the access policies
|
|
read in the "mmulist" section. Any memory ranges not specified
|
|
in "mmulist" are marked non-present.
|
|
|
|
If Kernel Page Table Isolation (CONFIG_X86_KPTI) is enabled, this
|
|
script additionally outputs a second set of page tables intended
|
|
for use by user threads running in Ring 3. These tables have the
|
|
same policy as the kernel's set of page tables with one crucial
|
|
difference: any pages not accessible to user mode threads are not
|
|
marked 'present', preventing Meltdown-style side channel attacks
|
|
from reading their contents.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import struct
|
|
from collections import namedtuple
|
|
import ctypes
|
|
import argparse
|
|
from elftools.elf.elffile import ELFFile
|
|
from elftools.elf.sections import SymbolTableSection
|
|
|
|
mmu_region_details = namedtuple("mmu_region_details",
|
|
"pde_index page_entries_info")
|
|
|
|
valid_pages_inside_pde = namedtuple("valid_pages_inside_pde", "start_addr size \
|
|
pte_valid_addr_start \
|
|
pte_valid_addr_end \
|
|
permissions")
|
|
|
|
mmu_region_details_pdpt = namedtuple("mmu_region_details_pdpt",
|
|
"pdpte_index pd_entries")
|
|
|
|
# Constants
|
|
PAGE_ENTRY_PRESENT = 1
|
|
PAGE_ENTRY_READ_WRITE = 1 << 1
|
|
PAGE_ENTRY_USER_SUPERVISOR = 1 << 2
|
|
PAGE_ENTRY_XD = 1 << 63
|
|
|
|
# Struct formatters
|
|
struct_mmu_regions_format = "<IIQ"
|
|
header_values_format = "<II"
|
|
page_entry_format = "<Q"
|
|
|
|
entry_counter = 0
|
|
def print_code(val):
|
|
global entry_counter
|
|
|
|
if not val & PAGE_ENTRY_PRESENT:
|
|
ret = '.'
|
|
else:
|
|
if val & PAGE_ENTRY_READ_WRITE:
|
|
# Writable page
|
|
if val & PAGE_ENTRY_XD:
|
|
# Readable, writeable, not executable
|
|
ret = 'w'
|
|
else:
|
|
# Readable, writable, executable
|
|
ret = 'a'
|
|
else:
|
|
# Read-only
|
|
if val & PAGE_ENTRY_XD:
|
|
# Read-only
|
|
ret = 'r'
|
|
else:
|
|
# Readable, executable
|
|
ret = 'x'
|
|
|
|
if val & PAGE_ENTRY_USER_SUPERVISOR:
|
|
# User accessible pages are capital letters
|
|
ret = ret.upper()
|
|
|
|
sys.stdout.write(ret)
|
|
entry_counter = entry_counter + 1
|
|
if entry_counter == 128:
|
|
sys.stdout.write("\n")
|
|
entry_counter = 0
|
|
|
|
class PageMode_PAE:
|
|
total_pages = 511
|
|
|
|
size_addressed_per_pde = (512 * 4096) # 2MB In Bytes
|
|
size_addressed_per_pdpte = (512 * size_addressed_per_pde) # In Bytes
|
|
list_of_pdpte = {}
|
|
|
|
def __init__(self, pd_start_addr, mem_regions, syms, kpti):
|
|
self.pd_start_addr = pd_start_addr
|
|
self.mem_regions = mem_regions
|
|
self.pd_tables_list = []
|
|
self.output_offset = 0
|
|
self.kpti = kpti
|
|
self.syms = syms
|
|
|
|
for i in range(4):
|
|
self.list_of_pdpte[i] = mmu_region_details_pdpt(pdpte_index=i,
|
|
pd_entries={})
|
|
self.populate_required_structs()
|
|
self.pdpte_create_binary_file()
|
|
self.page_directory_create_binary_file()
|
|
self.page_table_create_binary_file()
|
|
|
|
# return the pdpte number for the give address
|
|
def get_pdpte_number(self, value):
|
|
return (value >> 30) & 0x3
|
|
|
|
# return the page directory number for the give address
|
|
def get_pde_number(self, value):
|
|
return (value >> 21) & 0x1FF
|
|
|
|
# return the page table number for the given address
|
|
def get_pte_number(self, value):
|
|
return (value >> 12) & 0x1FF
|
|
|
|
def get_number_of_pd(self):
|
|
return len(self.get_pdpte_list())
|
|
|
|
def get_pdpte_list(self):
|
|
return list({temp[0] for temp in self.pd_tables_list})
|
|
|
|
# the return value will have the page address and it is assumed to be a 4096
|
|
# boundary.hence the output of this API will be a 20bit address of the page
|
|
# table
|
|
def address_of_page_table(self, pdpte, page_table_number):
|
|
# first page given to page directory pointer
|
|
# and 2nd page till 5th page are used for storing the page directories.
|
|
|
|
# set the max pdpte used. this tells how many pd are needed after
|
|
# that we start keeping the pt
|
|
PT_start_addr = self.get_number_of_pd() * 4096 +\
|
|
self.pd_start_addr + 4096
|
|
return (PT_start_addr +
|
|
(self.pd_tables_list.index([pdpte, page_table_number]) *
|
|
4096) >> 12)
|
|
|
|
def get_binary_pde_value(self, pdpte, value):
|
|
perms = value.page_entries_info[0].permissions
|
|
|
|
present = PAGE_ENTRY_PRESENT
|
|
|
|
read_write = check_bits(perms, [1, 29]) << 1
|
|
user_mode = check_bits(perms, [2, 28]) << 2
|
|
|
|
page_table = self.address_of_page_table(pdpte, value.pde_index) << 12
|
|
return present | read_write | user_mode | page_table
|
|
|
|
def get_binary_pte_value(self, value, pde, pte, perm_for_pte):
|
|
read_write = perm_for_pte & PAGE_ENTRY_READ_WRITE
|
|
user_mode = perm_for_pte & PAGE_ENTRY_USER_SUPERVISOR
|
|
xd = perm_for_pte & PAGE_ENTRY_XD
|
|
|
|
# This points to the actual memory in the HW
|
|
# totally 20 bits to rep the phy address
|
|
# first 2bits is from pdpte then 9bits is the number got from pde and
|
|
# next 9bits is pte
|
|
page_table = ((value.pdpte_index << 18) | (pde << 9) | pte) << 12
|
|
|
|
if self.kpti:
|
|
if user_mode:
|
|
present = PAGE_ENTRY_PRESENT
|
|
else:
|
|
if page_table == self.syms['z_shared_kernel_page_start']:
|
|
present = PAGE_ENTRY_PRESENT
|
|
else:
|
|
present = 0
|
|
else:
|
|
present = PAGE_ENTRY_PRESENT
|
|
|
|
binary_value = (present | read_write | user_mode | xd)
|
|
|
|
# L1TF mitigation: map non-present pages to the NULL page
|
|
if present:
|
|
binary_value |= page_table
|
|
|
|
return binary_value
|
|
|
|
def clean_up_unused_pdpte(self):
|
|
self.list_of_pdpte = {key: value for key, value in
|
|
self.list_of_pdpte.items()
|
|
if value.pd_entries != {}}
|
|
|
|
# update the tuple values for the memory regions needed
|
|
def set_pde_pte_values(self, pdpte, pde_index, address, mem_size,
|
|
pte_valid_addr_start, pte_valid_addr_end, perm):
|
|
|
|
pages_tuple = valid_pages_inside_pde(
|
|
start_addr=address,
|
|
size=mem_size,
|
|
pte_valid_addr_start=pte_valid_addr_start,
|
|
pte_valid_addr_end=pte_valid_addr_end,
|
|
permissions=perm)
|
|
|
|
mem_region_values = mmu_region_details(pde_index=pde_index,
|
|
page_entries_info=[])
|
|
|
|
mem_region_values.page_entries_info.append(pages_tuple)
|
|
|
|
if pde_index in self.list_of_pdpte[pdpte].pd_entries.keys():
|
|
# this step adds the new page info to the exsisting pages info
|
|
self.list_of_pdpte[pdpte].pd_entries[pde_index].\
|
|
page_entries_info.append(pages_tuple)
|
|
else:
|
|
self.list_of_pdpte[pdpte].pd_entries[pde_index] = mem_region_values
|
|
|
|
def populate_required_structs(self):
|
|
for start, size, flags in self.mem_regions:
|
|
pdpte_index = self.get_pdpte_number(start)
|
|
pde_index = self.get_pde_number(start)
|
|
pte_valid_addr_start = self.get_pte_number(start)
|
|
|
|
# Get the end of the page table entries
|
|
# Since a memory region can take up only a few entries in the Page
|
|
# table, this helps us get the last valid PTE.
|
|
pte_valid_addr_end = self.get_pte_number(start +
|
|
size - 1)
|
|
|
|
mem_size = size
|
|
|
|
# In-case the start address aligns with a page table entry other
|
|
# than zero and the mem_size is greater than (1024*4096) i.e 4MB
|
|
# in case where it overflows the currenty PDE's range then limit the
|
|
# PTE to 1024 and so make the mem_size reflect the actual size
|
|
# taken up in the current PDE
|
|
if (size + (pte_valid_addr_start * 4096)) >= \
|
|
(self.size_addressed_per_pde):
|
|
|
|
pte_valid_addr_end = self.total_pages
|
|
mem_size = (((self.total_pages + 1) -
|
|
pte_valid_addr_start) * 4096)
|
|
|
|
self.set_pde_pte_values(pdpte_index,
|
|
pde_index,
|
|
start,
|
|
mem_size,
|
|
pte_valid_addr_start,
|
|
pte_valid_addr_end,
|
|
flags)
|
|
|
|
if [pdpte_index, pde_index] not in self.pd_tables_list:
|
|
self.pd_tables_list.append([pdpte_index, pde_index])
|
|
|
|
# IF the current pde couldn't fit the entire requested region
|
|
# size then there is a need to create new PDEs to match the size.
|
|
# Here the overflow_size represents the size that couldn't be fit
|
|
# inside the current PDE, this is will now to used to create a new
|
|
# PDE/PDEs so the size remaining will be
|
|
# requested size - allocated size(in the current PDE)
|
|
|
|
overflow_size = size - mem_size
|
|
|
|
# create all the extra PDEs needed to fit the requested size
|
|
# this loop starts from the current pde till the last pde that is
|
|
# needed the last pde is calcualted as the (start_addr + size) >>
|
|
# 22
|
|
if overflow_size != 0:
|
|
for extra_pdpte in range(pdpte_index,
|
|
self.get_pdpte_number(start +
|
|
size) + 1):
|
|
for extra_pde in range(pde_index + 1, self.get_pde_number(
|
|
start + size) + 1):
|
|
|
|
# new pde's start address
|
|
# each page directory entry has a addr range of
|
|
# (1024 *4096) thus the new PDE start address is a
|
|
# multiple of that number
|
|
extra_pde_start_address = (
|
|
extra_pde * (self.size_addressed_per_pde))
|
|
|
|
# the start address of and extra pde will always be 0
|
|
# and the end address is calculated with the new
|
|
# pde's start address and the overflow_size
|
|
extra_pte_valid_addr_end = (
|
|
self.get_pte_number(extra_pde_start_address +
|
|
overflow_size - 1))
|
|
|
|
# if the overflow_size couldn't be fit inside this new
|
|
# pde then need another pde and so we now need to limit
|
|
# the end of the PTE to 1024 and set the size of this
|
|
# new region to the max possible
|
|
extra_region_size = overflow_size
|
|
if overflow_size >= (self.size_addressed_per_pde):
|
|
extra_region_size = self.size_addressed_per_pde
|
|
extra_pte_valid_addr_end = self.total_pages
|
|
|
|
# load the new PDE's details
|
|
|
|
self.set_pde_pte_values(extra_pdpte,
|
|
extra_pde,
|
|
extra_pde_start_address,
|
|
extra_region_size,
|
|
0,
|
|
extra_pte_valid_addr_end,
|
|
flags)
|
|
|
|
# for the next iteration of the loop the size needs
|
|
# to decreased
|
|
overflow_size -= extra_region_size
|
|
|
|
if [extra_pdpte, extra_pde] not in self.pd_tables_list:
|
|
self.pd_tables_list.append([extra_pdpte, extra_pde])
|
|
|
|
if overflow_size == 0:
|
|
break
|
|
|
|
self.pd_tables_list.sort()
|
|
self.clean_up_unused_pdpte()
|
|
|
|
|
|
pages_for_pdpte = 1
|
|
pages_for_pd = self.get_number_of_pd()
|
|
pages_for_pt = len(self.pd_tables_list)
|
|
self.output_buffer = ctypes.create_string_buffer((pages_for_pdpte +
|
|
pages_for_pd +
|
|
pages_for_pt) * 4096)
|
|
|
|
def pdpte_create_binary_file(self):
|
|
# pae needs a pdpte at 32byte aligned address
|
|
|
|
# Even though we have only 4 entries in the pdpte we need to move
|
|
# the self.output_offset variable to the next page to start pushing
|
|
# the pd contents
|
|
#
|
|
# FIXME: This wastes a ton of RAM!!
|
|
if args.verbose:
|
|
print("PDPTE at 0x%x" % self.pd_start_addr)
|
|
|
|
for pdpte in range(self.total_pages + 1):
|
|
if pdpte in self.get_pdpte_list():
|
|
present = 1 << 0
|
|
addr_of_pd = (((self.pd_start_addr + 4096) +
|
|
self.get_pdpte_list().index(pdpte) *
|
|
4096) >> 12) << 12
|
|
binary_value = (present | addr_of_pd)
|
|
else:
|
|
binary_value = 0
|
|
|
|
struct.pack_into(page_entry_format,
|
|
self.output_buffer,
|
|
self.output_offset,
|
|
binary_value)
|
|
|
|
self.output_offset += struct.calcsize(page_entry_format)
|
|
|
|
|
|
def page_directory_create_binary_file(self):
|
|
for pdpte, pde_info in self.list_of_pdpte.items():
|
|
if args.verbose:
|
|
print("Page directory %d at 0x%x" % (pde_info.pdpte_index,
|
|
self.pd_start_addr + self.output_offset))
|
|
for pde in range(self.total_pages + 1):
|
|
binary_value = 0 # the page directory entry is not valid
|
|
|
|
# if i have a valid entry to populate
|
|
if pde in pde_info.pd_entries.keys():
|
|
value = pde_info.pd_entries[pde]
|
|
binary_value = self.get_binary_pde_value(pdpte, value)
|
|
|
|
struct.pack_into(page_entry_format,
|
|
self.output_buffer,
|
|
self.output_offset,
|
|
binary_value)
|
|
if args.verbose:
|
|
print_code(binary_value)
|
|
|
|
self.output_offset += struct.calcsize(page_entry_format)
|
|
|
|
def page_table_create_binary_file(self):
|
|
for _, pde_info in sorted(self.list_of_pdpte.items()):
|
|
for pde, pte_info in sorted(pde_info.pd_entries.items()):
|
|
pe_info = pte_info.page_entries_info[0]
|
|
start_addr = pe_info.start_addr & ~0x1FFFFF
|
|
end_addr = start_addr + 0x1FFFFF
|
|
if args.verbose:
|
|
print("Page table for 0x%08x - 0x%08x at 0x%08x" %
|
|
(start_addr, end_addr,
|
|
self.pd_start_addr + self.output_offset))
|
|
for pte in range(self.total_pages + 1):
|
|
binary_value = 0 # the page directory entry is not valid
|
|
|
|
valid_pte = 0
|
|
# go through all the valid pages inside the pde to
|
|
# figure out if we need to populate this pte
|
|
for i in pte_info.page_entries_info:
|
|
temp_value = ((pte >= i.pte_valid_addr_start) and
|
|
(pte <= i.pte_valid_addr_end))
|
|
if temp_value:
|
|
perm_for_pte = i.permissions
|
|
valid_pte |= temp_value
|
|
|
|
# if i have a valid entry to populate
|
|
if valid_pte:
|
|
binary_value = self.get_binary_pte_value(pde_info,
|
|
pde,
|
|
pte,
|
|
perm_for_pte)
|
|
|
|
if args.verbose:
|
|
print_code(binary_value)
|
|
struct.pack_into(page_entry_format,
|
|
self.output_buffer,
|
|
self.output_offset,
|
|
binary_value)
|
|
self.output_offset += struct.calcsize(page_entry_format)
|
|
|
|
|
|
|
|
#*****************************************************************************#
|
|
|
|
def read_mmu_list(mmu_list_data):
|
|
regions = []
|
|
|
|
# Read mmu_list header data
|
|
num_of_regions, pd_start_addr = struct.unpack_from(
|
|
header_values_format, mmu_list_data, 0)
|
|
|
|
# a offset used to remember next location to read in the binary
|
|
size_read_from_binary = struct.calcsize(header_values_format)
|
|
|
|
if args.verbose:
|
|
print("Start address of page tables: 0x%08x" % pd_start_addr)
|
|
print("Build-time memory regions:")
|
|
|
|
# Read all the region definitions
|
|
for region_index in range(num_of_regions):
|
|
addr, size, flags = struct.unpack_from(struct_mmu_regions_format,
|
|
mmu_list_data,
|
|
size_read_from_binary)
|
|
size_read_from_binary += struct.calcsize(struct_mmu_regions_format)
|
|
|
|
if args.verbose:
|
|
print(" Region %03d: 0x%08x - 0x%08x (0x%016x)" %
|
|
(region_index, addr, addr + size - 1, flags))
|
|
|
|
# ignore zero sized memory regions
|
|
if size == 0:
|
|
continue
|
|
|
|
if (addr & 0xFFF) != 0:
|
|
print("Memory region %d start address %x is not page-aligned" %
|
|
(region_index, addr))
|
|
sys.exit(2)
|
|
|
|
if (size & 0xFFF) != 0:
|
|
print("Memory region %d size %d is not page-aligned" %
|
|
(region_index, size))
|
|
sys.exit(2)
|
|
|
|
# validate for memory overlap here
|
|
for other_region_index in range(len(regions)):
|
|
other_addr, other_size, _ = regions[other_region_index]
|
|
|
|
end_addr = addr + size
|
|
other_end_addr = other_addr + other_size
|
|
|
|
overlap = ((addr <= other_addr and end_addr > other_addr) or
|
|
(other_addr <= addr and other_end_addr > addr))
|
|
|
|
if overlap:
|
|
print("Memory region %d (%x:%x) overlaps memory region %d (%x:%x)" %
|
|
(region_index, addr, end_addr, other_region_index,
|
|
other_addr, other_end_addr))
|
|
sys.exit(2)
|
|
|
|
# add the retrived info another list
|
|
regions.append((addr, size, flags))
|
|
|
|
return (pd_start_addr, regions)
|
|
|
|
|
|
def check_bits(val, bits):
|
|
for b in bits:
|
|
if val & (1 << b):
|
|
return 1
|
|
return 0
|
|
|
|
def get_symbols(obj):
|
|
for section in obj.iter_sections():
|
|
if isinstance(section, SymbolTableSection):
|
|
return {sym.name: sym.entry.st_value
|
|
for sym in section.iter_symbols()}
|
|
|
|
raise LookupError("Could not find symbol table")
|
|
|
|
# Read the parameters passed to the file
|
|
def parse_args():
|
|
global args
|
|
|
|
parser = argparse.ArgumentParser(
|
|
description=__doc__,
|
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
|
|
parser.add_argument("-k", "--kernel",
|
|
help="Zephyr kernel image")
|
|
parser.add_argument("-o", "--output",
|
|
help="Output file into which the page tables are "
|
|
"written.")
|
|
parser.add_argument("-u", "--user-output",
|
|
help="User mode page tables for KPTI")
|
|
parser.add_argument("-v", "--verbose", action="count", default=0,
|
|
help="Print debugging information. Multiple "
|
|
"invocations increase verbosity")
|
|
args = parser.parse_args()
|
|
if "VERBOSE" in os.environ:
|
|
args.verbose = 1
|
|
|
|
def main():
|
|
parse_args()
|
|
|
|
with open(args.kernel, "rb") as fp:
|
|
kernel = ELFFile(fp)
|
|
syms = get_symbols(kernel)
|
|
irq_data = kernel.get_section_by_name("mmulist").data()
|
|
|
|
pd_start_addr, regions = read_mmu_list(irq_data)
|
|
|
|
# select the page table needed
|
|
page_table = PageMode_PAE(pd_start_addr, regions, syms, False)
|
|
|
|
# write the binary data into the file
|
|
with open(args.output, 'wb') as fp:
|
|
fp.write(page_table.output_buffer)
|
|
|
|
if "CONFIG_X86_KPTI" in syms:
|
|
pd_start_addr += page_table.output_offset
|
|
|
|
user_page_table = PageMode_PAE(pd_start_addr, regions, syms, True)
|
|
with open(args.user_output, 'wb') as fp:
|
|
fp.write(user_page_table.output_buffer)
|
|
|
|
if __name__ == "__main__":
|
|
main()
|