在对象文件中,函数 "main "的代码从哪里开始?

问题描述 投票:0回答:1

我有一个打印hello world的C程序的对象文件,我想用readelf工具或gdb或hexedit(我不知道哪个工具是正确的)来理解函数 "main "的代码在文件中的什么地方开始。

我用readelf知道符号_start &main发生的位置,以及它在虚拟内存中映射的地址。此外,我还知道.text部分的大小和指定的入口点,即文本部分的地址。

问题是--函数 "main "的代码从文件的哪里开始?我以为那是文本部分的入口点和偏移量,但我的理解是文本部分的数据、bss、rodata应该在main之前运行,在readelf中出现在文本部分之后。

另外,我以为我们应该在符号表中把所有行的大小加起来,直到main,但我不确定这是否正确。

另外一个问题是,如果我想用NOP instrcutres替换main函数,或者在我的对象文件中植入一条ret指令,我如何知道我可以用hexedit做的偏移量。

c assembly memory gdb elf
1个回答
2
投票

那么,让我们一步步来了解一下。

从这个C文件开始。

#include <stdio.h>

void printit()
{
    puts("Hello world!");
}

int main(void)
{
    printit();
    return 0;
}

根据注释,你是在x86系统上,编译成32位非PIE可执行文件,像这样。

$ gcc -m32 -no-pie  -o test test.c

这个... -m32 选项是需要的,因为我是在x86-64机器上工作。正如你已经知道的,你可以使用readelf、objdump或者nm来获取main的虚拟内存地址,比如像这样。

$ nm test | grep -w main
0804918d T main

很明显, 804918d 不能是文件中只有15 kB大的偏移量。您需要找到 虚拟内存地址文件偏移量. 在一个典型的ELF文件中,映射包括 两次. 一次是详细的形式,用于链接器(因为对象文件也是ELF文件)和调试器,第二次是浓缩的形式,用于内核加载程序。详细的形式是由节头组成的节列表,你可以这样查看(输出的内容缩短了一些,使答案更易读)。

$ readelf --section-headers test
There are 29 section headers, starting at offset 0x3748:

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
[...]
  [11] .init             PROGBITS        08049000 001000 000020 00  AX  0   0  4
  [12] .plt              PROGBITS        08049020 001020 000030 04  AX  0   0 16
  [13] .text             PROGBITS        08049050 001050 0001c1 00  AX  0   0 16
  [14] .fini             PROGBITS        08049214 001214 000014 00  AX  0   0  4
  [15] .rodata           PROGBITS        0804a000 002000 000015 00   A  0   0  4
[...]
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  p (processor specific)

这里你会发现 .text 节始地址 08049050 并有一个大小为 1c1 字节,所以它在地址 08049211. 主要地址: 804918d 在这个范围内,所以你知道 main 是文本部分的成员。如果从main的地址中减去文本部分的基数,你会发现main是 13d 字节中。节列表还包含文本部分数据开始的文件偏移量。它是 1050所以main的第一个字节在偏移量为 0x1050 + 0x13d == 0x118d.

你可以用程序头做同样的计算。

$ readelf --program-headers test
[...]
Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  PHDR           0x000034 0x08048034 0x08048034 0x00160 0x00160 R   0x4
  INTERP         0x000194 0x08048194 0x08048194 0x00013 0x00013 R   0x1
      [Requesting program interpreter: /lib/ld-linux.so.2]
  LOAD           0x000000 0x08048000 0x08048000 0x002e8 0x002e8 R   0x1000
  LOAD           0x001000 0x08049000 0x08049000 0x00228 0x00228 R E 0x1000
  LOAD           0x002000 0x0804a000 0x0804a000 0x0019c 0x0019c R   0x1000
  LOAD           0x002f0c 0x0804bf0c 0x0804bf0c 0x00110 0x00114 RW  0x1000
[...]

第二个加载行告诉你,面积 08049000 (VirtAddr)到 08049228 (VirtAddr + MemSiz)是可读和可执行的,并从偏移量的 1000 的文件中。所以又可以计算出main的地址是 18d 字节到这个加载区域,所以它必须驻留在偏移量为 0x118d 内的可执行文件。让我们来测试一下。

$ ./test
Hello world!
$ echo -ne '\xc3' | dd of=test conv=notrunc bs=1 count=1 seek=$((0x118d))
1+0 records in
1+0 records out
1 byte copied, 0.0116672 s, 0.1 kB/s
$ ./test
$

将main的第一个字节覆盖在... 0xc3在x86上,返回(near)的操作码,导致程序不再输出任何东西。


2
投票

_start 通常属于一个模块( *.o 文件)是固定的(在不同的系统上有不同的叫法,但常见的名称是 crt0.o 用汇编器编写)。) 那段固定的代码准备堆栈(通常参数和环境都被存放在初始堆栈段,由 execve(2) 系统调用)的使命。crt0.s 是准备初始C栈框架,并调用 main(). 一旦 main() 结束,它负责从main中获取返回值并调用所有的 atexit() 处理程序来完成对 _exit(2) 系统调用。

的连接。crt0.o 通常是透明的,因为你总是调用编译器自己做链接,所以你通常不需要添加 crt0.o 作为第一个对象模块,但编译器知道(最近,所有这些东西都大大增加了,因为我们依靠架构和ABI在函数之间传递参数)

如果你在执行编译器时使用 -v 选项,你将得到它用来调用链接器的准确命令行,你将得到你的程序在第一阶段的最终内存图的秘密。

© www.soinside.com 2019 - 2024. All rights reserved.