我有一个简单的调试器(使用ptrace:http://pastebin.com/D0um3bUi)来计算为给定输入可执行程序执行的指令数。它使用ptrace单步执行模式来计算指令。
为此,当程序1)的可执行文件(来自gcc main.c的a.out)作为输入提供给我的测试调试器时,它会在执行指令时打印大约100k。当我使用-static
选项时,它会提供10681条指令。
现在在2)我创建一个汇编程序并使用NASM进行编译和链接,然后当这个可执行文件作为测试调试器输入时,它显示8个指令作为计数,哪个是apt。
程序1)中执行的指令数量很高,因为在运行时将程序与系统库链接起来了?使用-static并将计数减少1/10。如何确保指令计数仅是程序1中主函数的指令,以及程序2)为调试器报告的方式?
1)
#include <stdio.h>
int main()
{
printf("Hello, world!\n");
return 0;
}
我使用gcc创建可执行文件。
2)
; 64-bit "Hello World!" in Linux NASM
global _start ; global entry point export for ld
section .text
_start:
; sys_write(stdout, message, length)
mov rax, 1 ; sys_write
mov rdi, 1 ; stdout
mov rsi, message ; message address
mov rdx, length ; message string length
syscall
; sys_exit(return_code)
mov rax, 60 ; sys_exit
mov rdi, 0 ; return 0 (success)
syscall
section .data
message: db 'Hello, world!',0x0A ; message and newline
length: equ $-message ; NASM definition pseudo-
nasm -f elf64 -o main.o -s main.asm
ld -o main main.o
答案 0 :(得分:6)
_start
。
您的 NASM 代码使用全局标签_start
,因此当您的程序运行时,程序中的第一个代码将是_start
的说明。使用 GCC 时,程序的典型入口点是函数main
。您隐藏的是 C 程序还有一个_start
标签,但它由 C 运行时启动对象提供。
现在的问题是 - 有没有办法绕过 C 启动文件,以便可以避免启动代码?技术上是的,但这是危险的领域,可能产生不确定的行为。如果您有冒险精神,您实际上可以通过-e
命令行选项告诉 GCC 更改程序的入口点。而不是_start
我们可以使我们的入口点main
绕过 C 启动代码。由于我们绕过 C 启动代码,我们还可以省去 C 运行时启动代码中的-nostartfiles
选项链接。
您可以使用此命令行编译 C 程序:
gcc test.c -e main -nostartfiles
不幸的是,必须在 C 代码中修复一些gotchya。通常,在使用 C 运行时启动对象时,在初始化环境后,会对main
进行 CALL 。通常main
执行 RET 指令,该指令返回 C 运行时代码。此时, C 运行时会正常退出程序。使用-nostartfiles
选项时, RET 无处可返回,因此可能会出现段错误。为了解决这个问题,我们可以调用 C 库_exit
函数来退出程序。
#include <stdio.h>
int main()
{
printf("Hello, world!\n");
_exit(0); /* We exit application here, never reaching the return */
return 0;
}
除非省略帧指针,否则 GCC 会发出一些额外指令来设置堆栈帧并将其拆除,但开销很小。
上述过程似乎不适用于标准glibc C 库中的静态构建( GCC 中的-static
选项)。这在Stackoverflow answer中讨论。动态版本有效,因为共享对象可以注册由动态加载程序调用以执行初始化的函数。静态构建时,这通常由 C 运行时完成,但我们已经跳过了初始化。因为像printf
这样的 GLIBC 函数可能会失败。有符合标准的替换 C 库,无需 C 运行时初始化即可运行。其中一种产品是MUSL。
在Ubuntu 64位上,这些命令应该构建并安装64位版本的 MUSL :
git clone git://git.musl-libc.org/musl
cd musl
./configure --prefix=/usr/local/musl/x86-64
make
sudo make install
然后,您可以使用 MUSL GCC 包装器来代替 MUSL 的 C 库在大多数Linux发行版上的默认 GLIBC 库。参数就像 GCC ,所以你应该能够:
/usr/local/musl/x86-64/bin/musl-gcc -e main -static -nostartfiles test.c
运行使用 GLIBC 生成的./a.out
时,可能会出现段错误。在使用大多数 C 库函数之前, MUSL 不需要初始化,因此即使使用-static
GCC 也可以使用它选项。
您比较的一个问题是,您直接在 NASM 中调用 SYS_WRITE 系统调用,在 C 中使用{{ 1}}。用户EOF正确地评论说您可能希望通过调用 C 中的printf
函数而不是write
来使其更公平地进行比较。 printf
的开销要小得多。您可以修改您的代码:
write
这将比 NASM 的直接 SYS_WRITE 系统调用带来更多开销,但远远低于#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
char *str = "Hello, world\n";
write (STDOUT_FILENO, str, 13);
_exit(0);
return 0;
}
生成的开销。
我将发出警告,除了软件开发的一些边缘情况之外,代码审查中可能不会很好地利用这些代码和技巧。
答案 1 :(得分:5)
程序1)中执行的指令数量很高,因为在运行时将程序与系统库相关联?
是的,动态链接加上CRT(C运行时)启动文件。
使用了
-static
并将计数减少了1/10。
因此,只需离开CRT启动文件,这些文件在调用main
之前执行,然后执行。
如何确保指令计数仅为程序1中的主要功能``
测量空main
,然后从未来的测量值中减去该数字。
除非您的指令计数器更智能,并且查看可执行文件中的符号以查找其跟踪的进程,否则它无法确定哪些代码来自哪里。
以及程序2)报告调试器的方式。
那是因为 该程序中没有其他代码。并不是因为你以某种方式帮助调试器忽略了一些指令,而是你在没有任何指令的情况下制作了一个程序而你自己没有把它放在那里。
如果您希望在运行gcc输出时看到实际,gdb a.out
,b _start
,r
和单步执行。一旦深入了解调用树,您就会陷入困境。想要使用fin
来完成当前功能的执行,因为你不想单步执行100万条指令,甚至是10k。