【发布时间】:2022-01-15 05:01:36
【问题描述】:
在 64 位程序中,用于获取堆栈保护器的选择器:偏移量是 fs:0x28,其中 fs=0。这没有问题,因为在 64 位中我们有 MSR fs_base(设置为指向 TLS)并且 GDT 被完全忽略。
但是对于 32 位程序,堆栈保护器是从 gs:0x14 读取的。在 64 位系统上运行我们有 gs=0x63,在 32 位系统上 gs=0x33。这里没有 MSR,因为它们是在 x86_64 中引入的,所以 GDT 在这里起着重要的作用。
对这两种情况的值进行剖析,我们得到 RPL=3(这是预期的),描述符表选择器指示 GDT(Linux 中不使用 LDT),选择器指向索引为 12 的 64 位和索引条目6 代表 32 位。
使用内核模块,我能够检查 64 位 linux 中的该条目是否为 NULL!所以不明白TLS的地址是怎么解析的。
内核模块的相关部分如下:
void gdtread()
{
struct desc_ptr gdtr;
seg_descriptor* gdt_entry = NULL;
uint16_t tr;
int i;
asm("str %0" : "=m"(tr));
native_store_gdt(&gdtr); // equiv. to asm("sgdt %0" : "=m"(gdtr));
printk("GDT address: 0x%px, GDT size: %d bytes = %i entries\n",
(void*)gdtr.address, gdtr.size + 1, (gdtr.size + 1) / 8);
gdt_entry = (seg_descriptor*)gdtr.address;
for(i = 0; i < (gdtr.size + 1) / 8; i++)
{
if(tr >> 3 == i)
printk("Entry #%i:\t<--- TSS (RPL = %i)", i, tr & 3);
else
printk("Entry #%i:", i);
if(!((uint64_t*)gdt_entry)[i])
{
printk("\tNULL");
continue;
}
if(gdt_entry[i].s)
user_segment_desc(&gdt_entry[i]);
else
system_segment_desc((sys_seg_descriptor*)&gdt_entry[i++]);
}
}
在 64 位系统上输出以下内容:
[ 3817.191065] GDT address: 0xfffffe0000001000, GDT size: 128 bytes = 16 entries
[ 3817.191073] Entry #0:
[ 3817.191075] NULL
[ 3817.191078] Entry #1:
[ 3817.191081] Raw: 0x00cf9b000000ffff
[ 3817.191084] Base: 0x00000000
[ 3817.191088] Limit: 0xfffff
[ 3817.191091] Flags: 0xc09b
[ 3817.191096] Type = 0xb (Code, non conforming, readable, accessed)
[ 3817.191100] S = 0 (user)
[ 3817.191103] DPL = 0
[ 3817.191105] P = 1 (present)
[ 3817.191109] AVL = 0
[ 3817.191112] L = 0 (legacy mode)
[ 3817.191115] D/B = 1
[ 3817.191118] G = 1 (KiB)
[ 3817.191121] Entry #2:
[ 3817.191124] Raw: 0x00af9b000000ffff
[ 3817.191127] Base: 0x00000000
[ 3817.191130] Limit: 0xfffff
[ 3817.191133] Flags: 0xa09b
[ 3817.191137] Type = 0xb (Code, non conforming, readable, accessed)
[ 3817.191141] S = 0 (user)
[ 3817.191144] DPL = 0
[ 3817.191146] P = 1 (present)
[ 3817.191149] AVL = 0
[ 3817.191152] L = 1 (long mode)
[ 3817.191155] D/B = 0
[ 3817.191157] G = 1 (KiB)
[ 3817.191160] Entry #3:
[ 3817.191163] Raw: 0x00cf93000000ffff
[ 3817.191166] Base: 0x00000000
[ 3817.191169] Limit: 0xfffff
[ 3817.191171] Flags: 0xc093
[ 3817.191175] Type = 0x3 (Data, expand down, writable, accessed)
[ 3817.191178] S = 0 (user)
[ 3817.191181] DPL = 0
[ 3817.191183] P = 1 (present)
[ 3817.191186] AVL = 0
[ 3817.191189] L = 0
[ 3817.191191] D/B = 1
[ 3817.191194] G = 1 (KiB)
[ 3817.191197] Entry #4:
[ 3817.191199] Raw: 0x00cffb000000ffff
[ 3817.191202] Base: 0x00000000
[ 3817.191205] Limit: 0xfffff
[ 3817.191207] Flags: 0xc0fb
[ 3817.191211] Type = 0xb (Code, non conforming, readable, accessed)
[ 3817.191214] S = 0 (user)
[ 3817.191217] DPL = 3
[ 3817.191219] P = 1 (present)
[ 3817.191222] AVL = 0
[ 3817.191224] L = 0 (legacy mode)
[ 3817.191227] D/B = 1
[ 3817.191230] G = 1 (KiB)
[ 3817.191233] Entry #5:
[ 3817.191235] Raw: 0x00cff3000000ffff
[ 3817.191238] Base: 0x00000000
[ 3817.191241] Limit: 0xfffff
[ 3817.191243] Flags: 0xc0f3
[ 3817.191246] Type = 0x3 (Data, expand down, writable, accessed)
[ 3817.191250] S = 0 (user)
[ 3817.191252] DPL = 3
[ 3817.191255] P = 1 (present)
[ 3817.191258] AVL = 0
[ 3817.191260] L = 0
[ 3817.191262] D/B = 1
[ 3817.191265] G = 1 (KiB)
[ 3817.191268] Entry #6:
[ 3817.191270] Raw: 0x00affb000000ffff
[ 3817.191273] Base: 0x00000000
[ 3817.191276] Limit: 0xfffff
[ 3817.191278] Flags: 0xa0fb
[ 3817.191281] Type = 0xb (Code, non conforming, readable, accessed)
[ 3817.191284] S = 0 (user)
[ 3817.191287] DPL = 3
[ 3817.191289] P = 1 (present)
[ 3817.191292] AVL = 0
[ 3817.191295] L = 1 (long mode)
[ 3817.191298] D/B = 0
[ 3817.191300] G = 1 (KiB)
[ 3817.191303] Entry #7:
[ 3817.191306] NULL
[ 3817.191308] Entry #8: <--- TSS (RPL = 0)
[ 3817.191312] Raw: 0x00000000fffffe0000008b0030004087
[ 3817.191316] Base: 0xfffffe0000003000
[ 3817.191321] Limit: 0x04087
[ 3817.191324] Flags: 0x008b
[ 3817.191327] Type = 0xb (Busy 64-bit TSS)
[ 3817.191331] S = 1 (system)
[ 3817.191333] DPL = 0
[ 3817.191336] P = 1 (present)
[ 3817.191339] AVL = 0
[ 3817.191341] L = 0
[ 3817.191344] D/B = 0
[ 3817.191347] G = 0 (B)
[ 3817.191349] Entry #10:
[ 3817.191352] NULL
[ 3817.191355] Entry #11:
[ 3817.191358] NULL
[ 3817.191360] Entry #12:
[ 3817.191362] NULL
[ 3817.191365] Entry #13:
[ 3817.191367] NULL
[ 3817.191369] Entry #14:
[ 3817.191372] NULL
[ 3817.191374] Entry #15:
[ 3817.191377] Raw: 0x0040f50000000000
[ 3817.191380] Base: 0x00000000
[ 3817.191382] Limit: 0x00000
[ 3817.191385] Flags: 0x40f5
[ 3817.191389] Type = 0x5 (Data, expand up, read only, accessed)
[ 3817.191392] S = 0 (user)
[ 3817.191395] DPL = 3
[ 3817.191397] P = 1 (present)
[ 3817.191400] AVL = 0
[ 3817.191403] L = 0
[ 3817.191405] D/B = 1
[ 3817.191408] G = 0 (B)
我还没有在 32 位系统上尝试过这个模块,但我正在路上。
那么,把问题说清楚:gs 段选择器如何在 64 位 linux 内核上运行的 32 位程序中工作?
【问题讨论】:
-
我很确定 64 位内核可以在 64 位模式下使用 MSR(或
wrgsbase),然后再返回用户空间(或第一次进入)。所以你只需要在 32 位内核中处理 GDT。 -
@PeterCordes,当我在调试 32 位程序时在 gdb 中尝试
i r $gs_base时,我得到“无效寄存器”,所以我认为它们在传统模式下无法访问。但是您的评论让我在文档中进行搜索,特别是“AMD64 Architecture Programmer's Manual, vol. 2”,并在第 27 页中说 Compatibility mode 在计算时忽略 FS 和 GS 段描述符中基地址的高 32 位一个有效地址。 这意味着实际上 GS 和 FS 的基地址寄存器也用于传统模式。 -
在 64 位模式下,不使用段选择器。相反,一个特殊的 MSR 用于确定 FS 和 GS 的段基础。
-
@PeterCordes,确实如此!我修改内核模块以读取 32 位程序的 gs_base,是的,它指向 TLS,其内容为
0x00000000f7f84040。 -
您是否看到 GDB 使用
ptrace(PTRACE_POKETEXT)将rdgsbase插入目标进程并单步执行?这与在来宾线程的上下文中进行系统调用一样不可能。 (虽然strace -f无法证明这一点;一个过程一次只能被一件事追踪)。内核更有可能通过ptrace使基于段的寄存器可用,作为GDB 可以通过ptrace(PTRACE_PEEKUSER)或它实际使用的任何东西读取的线程架构状态的一部分。如果 32 位 ABI 不包含这些结构字段,可能仅适用于 64 位进程?
标签: linux assembly x86 memory-segmentation gdt