diff --git a/Cargo.lock b/Cargo.lock
index de28cba5..8284636c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -746,8 +746,7 @@ dependencies = [
 [[package]]
 name = "x86_64"
 version = "0.14.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "958cd5cb28e720db2f59ee9dc4235b5f82a183d079fb0e6caf43ad074cfdc66a"
+source = "git+https://github.com/Freax13/x86_64?rev=1335841#13358412a68efb5a401fcc6fa27a8edc30407410"
 dependencies = [
  "bit_field",
  "bitflags",
diff --git a/Cargo.toml b/Cargo.toml
index 55db05f7..f040ab52 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -102,3 +102,6 @@ pre-release-replacements = [
     { file = "Changelog.md", search = "# Unreleased", replace = "# Unreleased\n\n# {{version}} – {{date}}", exactly = 1 },
 ]
 pre-release-commit-message = "Release version {{version}}"
+
+[patch.crates-io]
+x86_64 = { git = "https://github.com/Freax13/x86_64", rev = "1335841" }
diff --git a/api/build.rs b/api/build.rs
index 142c2e72..1742b17f 100644
--- a/api/build.rs
+++ b/api/build.rs
@@ -15,13 +15,14 @@ fn main() {
         (31, 9),
         (40, 9),
         (49, 9),
-        (58, 10),
-        (68, 10),
-        (78, 1),
-        (79, 9),
+        (58, 9),
+        (67, 10),
+        (77, 10),
+        (87, 1),
         (88, 9),
         (97, 9),
         (106, 9),
+        (115, 9),
     ];
 
     let mut code = String::new();
diff --git a/api/src/config.rs b/api/src/config.rs
index 4f69174c..0df663c7 100644
--- a/api/src/config.rs
+++ b/api/src/config.rs
@@ -35,7 +35,7 @@ impl BootloaderConfig {
         0x3D,
     ];
     #[doc(hidden)]
-    pub const SERIALIZED_LEN: usize = 115;
+    pub const SERIALIZED_LEN: usize = 124;
 
     /// Creates a new default configuration with the following values:
     ///
@@ -72,6 +72,7 @@ impl BootloaderConfig {
             kernel_stack,
             boot_info,
             framebuffer,
+            gdt,
             physical_memory,
             page_table_recursive,
             aslr,
@@ -94,30 +95,31 @@ impl BootloaderConfig {
         let buf = concat_31_9(buf, kernel_stack.serialize());
         let buf = concat_40_9(buf, boot_info.serialize());
         let buf = concat_49_9(buf, framebuffer.serialize());
+        let buf = concat_58_9(buf, gdt.serialize());
 
-        let buf = concat_58_10(
+        let buf = concat_67_10(
             buf,
             match physical_memory {
                 Option::None => [0; 10],
                 Option::Some(m) => concat_1_9([1], m.serialize()),
             },
         );
-        let buf = concat_68_10(
+        let buf = concat_77_10(
             buf,
             match page_table_recursive {
                 Option::None => [0; 10],
                 Option::Some(m) => concat_1_9([1], m.serialize()),
             },
         );
-        let buf = concat_78_1(buf, [(*aslr) as u8]);
-        let buf = concat_79_9(
+        let buf = concat_87_1(buf, [(*aslr) as u8]);
+        let buf = concat_88_9(
             buf,
             match dynamic_range_start {
                 Option::None => [0; 9],
                 Option::Some(addr) => concat_1_8([1], addr.to_le_bytes()),
             },
         );
-        let buf = concat_88_9(
+        let buf = concat_97_9(
             buf,
             match dynamic_range_end {
                 Option::None => [0; 9],
@@ -125,14 +127,14 @@ impl BootloaderConfig {
             },
         );
 
-        let buf = concat_97_9(
+        let buf = concat_106_9(
             buf,
             match minimum_framebuffer_height {
                 Option::None => [0; 9],
                 Option::Some(addr) => concat_1_8([1], addr.to_le_bytes()),
             },
         );
-        let buf = concat_106_9(
+        let buf = concat_115_9(
             buf,
             match minimum_framebuffer_width {
                 Option::None => [0; 9],
@@ -189,6 +191,7 @@ impl BootloaderConfig {
             let (&kernel_stack, s) = split_array_ref(s);
             let (&boot_info, s) = split_array_ref(s);
             let (&framebuffer, s) = split_array_ref(s);
+            let (&gdt, s) = split_array_ref(s);
             let (&physical_memory_some, s) = split_array_ref(s);
             let (&physical_memory, s) = split_array_ref(s);
             let (&page_table_recursive_some, s) = split_array_ref(s);
@@ -203,6 +206,7 @@ impl BootloaderConfig {
                 kernel_stack: Mapping::deserialize(&kernel_stack)?,
                 boot_info: Mapping::deserialize(&boot_info)?,
                 framebuffer: Mapping::deserialize(&framebuffer)?,
+                gdt: Mapping::deserialize(&gdt)?,
                 physical_memory: match physical_memory_some {
                     [0] if physical_memory == [0; 9] => Option::None,
                     [1] => Option::Some(Mapping::deserialize(&physical_memory)?),
@@ -351,6 +355,8 @@ pub struct Mappings {
     pub boot_info: Mapping,
     /// Specifies the mapping of the frame buffer memory region.
     pub framebuffer: Mapping,
+    /// Specifies the mapping of the GDT.
+    pub gdt: Mapping,
     /// The bootloader supports to map the whole physical memory into the virtual address
     /// space at some offset. This is useful for accessing and modifying the page tables set
     /// up by the bootloader.
@@ -388,6 +394,7 @@ impl Mappings {
             kernel_stack: Mapping::new_default(),
             boot_info: Mapping::new_default(),
             framebuffer: Mapping::new_default(),
+            gdt: Mapping::new_default(),
             physical_memory: Option::None,
             page_table_recursive: Option::None,
             aslr: false,
@@ -404,6 +411,7 @@ impl Mappings {
             kernel_stack: Mapping::random(),
             boot_info: Mapping::random(),
             framebuffer: Mapping::random(),
+            gdt: Mapping::random(),
             physical_memory: if phys {
                 Option::Some(Mapping::random())
             } else {
diff --git a/common/src/lib.rs b/common/src/lib.rs
index 8b49945f..42e67809 100644
--- a/common/src/lib.rs
+++ b/common/src/lib.rs
@@ -8,13 +8,22 @@ use bootloader_api::{
     info::{FrameBuffer, FrameBufferInfo, MemoryRegion, TlsTemplate},
     BootInfo, BootloaderConfig,
 };
-use core::{alloc::Layout, arch::asm, mem::MaybeUninit, slice};
+use core::{
+    alloc::Layout,
+    arch::asm,
+    mem::{size_of, MaybeUninit},
+    slice,
+};
 use level_4_entries::UsedLevel4Entries;
-use usize_conversions::FromUsize;
+use usize_conversions::{FromUsize, IntoUsize};
 use x86_64::{
-    structures::paging::{
-        page_table::PageTableLevel, FrameAllocator, Mapper, OffsetPageTable, Page, PageSize,
-        PageTableFlags, PageTableIndex, PhysFrame, Size2MiB, Size4KiB,
+    instructions::tables::{lgdt, sgdt},
+    structures::{
+        gdt::GlobalDescriptorTable,
+        paging::{
+            page_table::PageTableLevel, FrameAllocator, Mapper, OffsetPageTable, Page, PageSize,
+            PageTable, PageTableFlags, PageTableIndex, PhysFrame, Size2MiB, Size4KiB,
+        },
     },
     PhysAddr, VirtAddr,
 };
@@ -145,6 +154,7 @@ where
     I: ExactSizeIterator<Item = D> + Clone,
     D: LegacyMemoryRegion,
 {
+    let bootloader_page_table = &mut page_tables.bootloader;
     let kernel_page_table = &mut page_tables.kernel;
 
     let mut used_entries = UsedLevel4Entries::new(
@@ -195,30 +205,25 @@ where
         }
     }
 
-    // identity-map context switch function, so that we don't get an immediate pagefault
-    // after switching the active page table
-    let context_switch_function = PhysAddr::new(context_switch as *const () as u64);
-    let context_switch_function_start_frame: PhysFrame =
-        PhysFrame::containing_address(context_switch_function);
-    for frame in PhysFrame::range_inclusive(
-        context_switch_function_start_frame,
-        context_switch_function_start_frame + 1,
-    ) {
-        match unsafe {
-            kernel_page_table.identity_map(frame, PageTableFlags::PRESENT, frame_allocator)
-        } {
-            Ok(tlb) => tlb.flush(),
-            Err(err) => panic!("failed to identity map frame {:?}: {:?}", frame, err),
-        }
-    }
-
     // create, load, and identity-map GDT (required for working `iretq`)
     let gdt_frame = frame_allocator
         .allocate_frame()
         .expect("failed to allocate GDT frame");
     gdt::create_and_load(gdt_frame);
+    let gdt = mapping_addr(
+        config.mappings.gdt,
+        u64::from_usize(size_of::<GlobalDescriptorTable>()),
+        4096,
+        &mut used_entries,
+    );
+    let gdt_page = Page::from_start_address(gdt).unwrap();
     match unsafe {
-        kernel_page_table.identity_map(gdt_frame, PageTableFlags::PRESENT, frame_allocator)
+        kernel_page_table.map_to(
+            gdt_page,
+            gdt_frame,
+            PageTableFlags::PRESENT,
+            frame_allocator,
+        )
     } {
         Ok(tlb) => tlb.flush(),
         Err(err) => panic!("failed to identity map frame {:?}: {:?}", gdt_frame, err),
@@ -319,6 +324,151 @@ where
         None
     };
 
+    // Setup memory for the context switch.
+    // We set up two regions of memory:
+    // 1. "context switch page" - This page contains only a single instruction
+    //    to switch to the kernel's page table. It's placed right before the
+    //    kernel's entrypoint, so that the last instruction the bootloader
+    //    executes is the page table switch and we don't need to jump to the
+    //    entrypoint.
+    // 2. "trampoline" - The "context switch page" might overlap with the
+    //    bootloader's memory, so we can't map it into the bootloader's address
+    //    space. Instead we map a trampoline at an address of our choosing and
+    //    jump to it instead. The trampoline will then switch to a new page
+    //    table (context switch page table) that contains the "context switch
+    //    page" and jump to it.
+
+    let phys_offset = kernel_page_table.phys_offset();
+    let translate_frame_to_virt = |frame: PhysFrame| phys_offset + frame.start_address().as_u64();
+
+    // The switching the page table is a 3 byte instruction.
+    // Check that subtraction 3 from the entrypoint won't jump the gap in the address space.
+    if (0xffff_8000_0000_0000..=0xffff_8000_0000_0002).contains(&entry_point.as_u64()) {
+        panic!("The kernel's entrypoint must not be located between 0xffff_8000_0000_0000 and 0xffff_8000_0000_0002");
+    }
+    // Determine the address where we should place the page table switch instruction.
+    let entrypoint_page: Page = Page::containing_address(entry_point);
+    let addr_just_before_entrypoint = entry_point.as_u64().wrapping_sub(3);
+    let context_switch_addr = VirtAddr::new(addr_just_before_entrypoint);
+    let context_switch_page: Page = Page::containing_address(context_switch_addr);
+
+    // Choose the address for the trampoline. The address shouldn't overlap
+    // with the bootloader's memory or the context switch page.
+    let trampoline_page_candidate1: Page =
+        Page::from_start_address(VirtAddr::new(0xffff_ffff_ffff_f000)).unwrap();
+    let trampoline_page_candidate2: Page =
+        Page::from_start_address(VirtAddr::new(0xffff_ffff_ffff_c000)).unwrap();
+    let trampoline_page = if context_switch_page != trampoline_page_candidate1
+        && entrypoint_page != trampoline_page_candidate1
+    {
+        trampoline_page_candidate1
+    } else {
+        trampoline_page_candidate2
+    };
+
+    // Prepare the trampoline.
+    let trampoline_frame = frame_allocator
+        .allocate_frame()
+        .expect("Failed to allocate memory for trampoline");
+    // Write two instructions to the trampoline:
+    // 1. Load the context switch page table
+    // 2. Jump to the context switch
+    unsafe {
+        let trampoline: *mut u8 = translate_frame_to_virt(trampoline_frame).as_mut_ptr();
+        // mov cr3, rdx
+        trampoline.add(0).write(0x0f);
+        trampoline.add(1).write(0x22);
+        trampoline.add(2).write(0xda);
+        // jmp r13
+        trampoline.add(3).write(0x41);
+        trampoline.add(4).write(0xff);
+        trampoline.add(5).write(0xe5);
+    }
+
+    // Write the instruction to switch to the final kernel page table to the context switch page.
+    let context_switch_frame = frame_allocator
+        .allocate_frame()
+        .expect("Failed to allocate memory for context switch page");
+    // mov cr3, rax
+    let instruction_bytes = [0x0f, 0x22, 0xd8];
+    let context_switch_ptr: *mut u8 = translate_frame_to_virt(context_switch_frame).as_mut_ptr();
+    for (i, b) in instruction_bytes.into_iter().enumerate() {
+        // We can let the offset wrap around because we map the frame twice
+        // if the context switch is near a page boundary.
+        let offset = (context_switch_addr.as_u64().into_usize()).wrapping_add(i) % 4096;
+
+        unsafe {
+            // Write the instruction byte.
+            context_switch_ptr.add(offset).write(b);
+        }
+    }
+
+    // Create a new page table for use during the context switch.
+    let context_switch_page_table_frame = frame_allocator
+        .allocate_frame()
+        .expect("Failed to allocate frame for context switch page table");
+    let context_switch_page_table: &mut PageTable = {
+        let ptr: *mut PageTable =
+            translate_frame_to_virt(context_switch_page_table_frame).as_mut_ptr();
+        // create a new, empty page table
+        unsafe {
+            ptr.write(PageTable::new());
+            &mut *ptr
+        }
+    };
+    let mut context_switch_page_table =
+        unsafe { OffsetPageTable::new(context_switch_page_table, phys_offset) };
+
+    // Map the trampoline and the context switch.
+    unsafe {
+        // Map the trampoline page into both the bootloader's page table and
+        // the context switch page table.
+        bootloader_page_table
+            .map_to(
+                trampoline_page,
+                trampoline_frame,
+                PageTableFlags::PRESENT,
+                frame_allocator,
+            )
+            .expect("Failed to map trampoline into main page table")
+            .ignore();
+        context_switch_page_table
+            .map_to(
+                trampoline_page,
+                trampoline_frame,
+                PageTableFlags::PRESENT,
+                frame_allocator,
+            )
+            .expect("Failed to map trampoline into context switch page table")
+            .ignore();
+
+        // Map the context switch only into the context switch page table.
+        context_switch_page_table
+            .map_to(
+                context_switch_page,
+                context_switch_frame,
+                PageTableFlags::PRESENT,
+                frame_allocator,
+            )
+            .expect("Failed to map context switch into context switch page table")
+            .ignore();
+
+        // If the context switch is near a page boundary, map the entrypoint
+        // page to the same frame in case the page table switch instruction
+        // crosses a page boundary.
+        if context_switch_page != entrypoint_page {
+            context_switch_page_table
+                .map_to(
+                    entrypoint_page,
+                    context_switch_frame,
+                    PageTableFlags::PRESENT,
+                    frame_allocator,
+                )
+                .expect("Failed to map context switch into context switch page table")
+                .ignore();
+        }
+    }
+
     Mappings {
         framebuffer: framebuffer_virt_addr,
         entry_point,
@@ -330,6 +480,11 @@ where
 
         kernel_slice_start,
         kernel_slice_len,
+        gdt,
+        context_switch_trampoline: trampoline_page.start_address(),
+        context_switch_page_table,
+        context_switch_page_table_frame,
+        context_switch_addr,
     }
 }
 
@@ -355,6 +510,16 @@ pub struct Mappings {
     pub kernel_slice_start: u64,
     /// Size of the kernel slice allocation in memory.
     pub kernel_slice_len: u64,
+    /// The address of the GDT in the kernel's address space.
+    pub gdt: VirtAddr,
+    /// The address of the context switch trampoline in the bootloader's address space.
+    pub context_switch_trampoline: VirtAddr,
+    /// The page table used for context switch from the bootloader to the kernel.
+    pub context_switch_page_table: OffsetPageTable<'static>,
+    /// The physical frame where the level 4 page table of the context switch address space is stored.
+    pub context_switch_page_table_frame: PhysFrame,
+    /// Address just before the kernel's entrypoint.
+    pub context_switch_addr: VirtAddr,
 }
 
 /// Allocates and initializes the boot info struct and the memory map.
@@ -470,15 +635,18 @@ pub fn switch_to_kernel(
         ..
     } = page_tables;
     let addresses = Addresses {
+        gdt: mappings.gdt,
+        context_switch_trampoline: mappings.context_switch_trampoline,
+        context_switch_page_table: mappings.context_switch_page_table_frame,
+        context_switch_addr: mappings.context_switch_addr,
         page_table: kernel_level_4_frame,
         stack_top: mappings.stack_end.start_address(),
-        entry_point: mappings.entry_point,
         boot_info,
     };
 
     log::info!(
-        "Jumping to kernel entry point at {:?}",
-        addresses.entry_point
+        "Switching to kernel entry point at {:?}",
+        mappings.entry_point
     );
 
     unsafe {
@@ -502,23 +670,40 @@ pub struct PageTables {
 
 /// Performs the actual context switch.
 unsafe fn context_switch(addresses: Addresses) -> ! {
+    // Update the GDT base address.
+    let mut gdt_pointer = sgdt();
+    gdt_pointer.base = addresses.gdt;
+    unsafe {
+        // SAFETY: Note that the base address points to memory that is only
+        //         mapped in the kernel's page table. We can do this because
+        //         just loading the GDT doesn't cause any immediate loads and
+        //         by the time the base address is dereferenced the context
+        //         switch will be done.
+        lgdt(&gdt_pointer);
+    }
+
     unsafe {
         asm!(
-            "mov cr3, {}; mov rsp, {}; push 0; jmp {}",
-            in(reg) addresses.page_table.start_address().as_u64(),
+            "mov rsp, {}; sub rsp, 8; jmp {}",
             in(reg) addresses.stack_top.as_u64(),
-            in(reg) addresses.entry_point.as_u64(),
+            in(reg) addresses.context_switch_trampoline.as_u64(),
+            in("rdx") addresses.context_switch_page_table.start_address().as_u64(),
+            in("r13") addresses.context_switch_addr.as_u64(),
+            in("rax") addresses.page_table.start_address().as_u64(),
             in("rdi") addresses.boot_info as *const _ as usize,
+            options(noreturn),
         );
     }
-    unreachable!();
 }
 
 /// Memory addresses required for the context switch.
 struct Addresses {
+    gdt: VirtAddr,
+    context_switch_trampoline: VirtAddr,
+    context_switch_page_table: PhysFrame,
+    context_switch_addr: VirtAddr,
     page_table: PhysFrame,
     stack_top: VirtAddr,
-    entry_point: VirtAddr,
     boot_info: &'static mut BootInfo,
 }