为什么我在没有stdlib的情况下链接汇编代码会变成僵尸?

为什么我在没有stdlib的情况下链接汇编代码会变成僵尸?,std,nasm,x86-64,ld,gtk3,Std,Nasm,X86 64,Ld,Gtk3,我在试验汇编代码和GTK+3库时发现,如果我不将对象文件与标准库gcc链接,我的应用程序就会变成僵尸。这是我的stdlib免费应用程序代码 %include "gtk.inc" %include "glib.inc" global _start SECTION .data destroy db "destroy", 0 ; const gchar* strWindow db "Window", 0 ; const gcha

我在试验汇编代码和GTK+3库时发现,如果我不将对象文件与标准库
gcc
链接,我的应用程序就会变成僵尸。这是我的
stdlib
免费应用程序代码

%include "gtk.inc"
%include "glib.inc"

global _start

SECTION .data    
destroy         db "destroy", 0     ; const gchar*
strWindow       db "Window", 0              ; const gchar*

SECTION .bss    
window         resq 1 ; GtkWindow *

SECTION .text    
_start:
    ; gtk_init (&argc, &argv);
    xor     rdi, rdi
    xor     rsi, rsi
    call    gtk_init

    ; window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    xor     rdi, rdi
    call    gtk_window_new
    mov     [window], rax

    ; gtk_window_set_title (GTK_WINDOW (window), "Window");
    mov     rdi, rax
    mov     rsi, strWindow
    call    gtk_window_set_title

    ; g_signal_connect (window, "destroy", G_CALLBACK (gtk_main_quit), NULL);
    mov     rdi, [window]
    mov     rsi, destroy
    mov     rdx, gtk_main_quit
    xor     rcx, rcx
    xor     r8, r8
    xor     r9, r9
    call    g_signal_connect_data

    ; gtk_widget_show (window);
    mov     rdi, [window]
    call    gtk_widget_show

    ; gtk_main ();
    call    gtk_main

    mov     rax, 60 ; SYS_EXIT
    xor     rdi, rdi
    syscall
这里是与标准库链接的相同代码

%include "gtk.inc"
%include "glib.inc"

global main

SECTION .data    
destroy         db "destroy", 0     ; const gchar*
strWindow       db "Window", 0              ; const gchar*

SECTION .bss
window         resq 1 ; GtkWindow *

SECTION .text    
main:
    push    rbp
    mov     rbp, rsp

    ; gtk_init (&argc, &argv);
    xor     rdi, rdi
    xor     rsi, rsi
    call    gtk_init

    ; window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    xor     rdi, rdi
    call    gtk_window_new
    mov     [window], rax

    ; gtk_window_set_title (GTK_WINDOW (window), "Window");
    mov     rdi, rax
    mov     rsi, strWindow
    call    gtk_window_set_title

    ; g_signal_connect (window, "destroy", G_CALLBACK (gtk_main_quit), NULL);
    mov     rdi, [window]
    mov     rsi, destroy
    mov     rdx, gtk_main_quit
    xor     rcx, rcx
    xor     r8, r8
    xor     r9, r9
    call    g_signal_connect_data

    ; gtk_widget_show (window);
    mov     rdi, [window]
    call    gtk_widget_show

    ; gtk_main ();
    call    gtk_main

    pop     rbp
    ret
两个应用程序都创建一个
GtkWindow
。但是,当窗口关闭时,两者的行为不同。前者导致一个僵尸进程,我需要按
Ctrl+C
。后者表现出预期的行为,即应用程序在窗口关闭后立即终止

我的感觉是,标准库正在执行一些我在第一个代码示例中忽略的基本操作,但我不知道它是什么


所以我的问题是:第一个代码示例中缺少了什么?

感谢@MichaelPetch的这个想法,它完美地解释了所有观察到的症状:

如果
gtk_main
在返回时让任何线程运行,那么两个程序之间最重要的区别是
eax=60
/
syscall
仅退出当前线程。请参阅中的文档,其中指出glibc的
\u exit()
包装函数自glibc2.3以来一直使用
exit\u组

在x86-64 ABI中是
eax=231
/
syscall
。这是当
main()
返回时CRT启动/清理代码运行的内容

./thread-exit &   # or in the foreground, and do the following commands in another shell
[1] 20592

$ ps m -LF -p $(pidof thread-exit)
UID        PID  PPID   LWP  C NLWP    SZ   RSS PSR STIME TTY      STAT   TIME CMD
peter    20592  7749     -  0    3 109031 21920  - 06:28 pts/12   -      0:00 ./thread-exit
peter        -     - 20592  0    -     -     -   0 06:28 -        Sl     0:00 -
peter        -     - 20593  0    -     -     -   0 06:28 -        Sl     0:00 -
peter        -     - 20594  0    -     -     -   0 06:28 -        Sl     0:00 -
您可以在两个版本上使用
strace./a.out
来查看这一点


这至少让我感到惊讶:一个初始线程已退出但其他线程仍在运行的进程显示为僵尸。我在自己的桌面上尝试过它(请参见本答案的结尾部分,了解构建命令和外部声明,这样您就不需要
gtk.inc
),您确实得到了一个报告为a的进程,但您可以在
gtk\u main
返回时按住ctrl-c键以杀死gtk运行的其他线程

./thread-exit &   # or in the foreground, and do the following commands in another shell
[1] 20592

$ ps m -LF -p $(pidof thread-exit)
UID        PID  PPID   LWP  C NLWP    SZ   RSS PSR STIME TTY      STAT   TIME CMD
peter    20592  7749     -  0    3 109031 21920  - 06:28 pts/12   -      0:00 ./thread-exit
peter        -     - 20592  0    -     -     -   0 06:28 -        Sl     0:00 -
peter        -     - 20593  0    -     -     -   0 06:28 -        Sl     0:00 -
peter        -     - 20594  0    -     -     -   0 06:28 -        Sl     0:00 -
然后关闭窗口:进程没有退出,仍然有两个线程运行+1僵尸

$ ps m -LF -p $(pidof thread-exit)
UID        PID  PPID   LWP  C NLWP    SZ   RSS PSR STIME TTY      STAT   TIME CMD
peter    20592  7749     -  0    3     0     0   - 06:28 pts/12   -      0:00 [thread-exit] <defunct>
peter        -     - 20592  0    -     -     -   0 06:28 -        Zl     0:00 -
peter        -     - 20593  0    -     -     -   0 06:28 -        Sl     0:00 -
peter        -     - 20594  0    -     -     -   0 06:28 -        Sl     0:00 -
如果您想保留帧指针的内容,您仍然可以执行tail调用,但是
pop rbp
/
jmp gtk_main


PS:对于那些想自己尝试的人来说,这个改变让你无需去寻找
gtk.inc

;%include "gtk.inc"
;%include "glib.inc"

extern gtk_init
extern gtk_window_new
extern g_signal_connect_data
extern gtk_window_set_title
extern gtk_widget_show
extern gtk_main
extern gtk_main_quit
使用以下内容构建:

yasm -felf64 -Worphan-labels -gdwarf2 thread-exit.asm &&
gcc -nostdlib -o thread-exit thread-exit.o $(pkg-config --libs gtk+-3.0)

mov-rax,60;SYS_EXIT xor rdi,rdi syscall
使正常关机程序短路。由于您没有展示如何组装/链接,而且这不是一个最小的可验证示例(您使用的标题不是问题的一部分),所以很难说。一种可能性是需要调用C库
exit
。即使没有调用sys\u exit,我最终还是会变成一个僵尸。生成%included文件很简单:它们只包含外部符号的外部声明。我使用了“nasm-f elf64…”和“ld-I/path/to/interpreter
pkg-config--libs gtk+-3.0
…”。我怀疑我需要调用其他东西,这可能是由C的退出完成的某种进程清理。然而,我不确定这次清理是什么。我试图包括一个带有-1作为pid的sys_wait4调用,但即使这样,我仍然会得到一个僵尸;SYS_EXIT xor rdi,rdi syscall
这将不起作用,因为这样您的程序可能会在随机内存中出错。我提到的C库
exit
函数通常会执行必要的清理(这可能会使您的程序与众不同),以便GTK正常关闭(我记得它还会执行一些与线程相关的清理)
gtk.inc
和另一个inc的生成可能微不足道,但如果您希望任何人认真对待这一点(或尝试您的代码),那么您可能希望提供它们。如果没有它们,这不是一个最小的完全可验证的例子。某些内容未初始化或未清理。如果它与线程相关,我不会感到惊讶。一个更大的问题是为什么要绕过C运行时?我不想包括C库的原因是因为我不明白为什么它们是必要的。我正在编写一个只需要从Gtk+3库调用API的Linux应用程序。感谢您的回答,
exit_group
解决了进程问题。关于僵尸,这是我关闭窗口后系统监视器报告的内容。这对我来说是有意义的,因为我正在终止父进程,而不用等待子进程的返回值。无论如何,我可以理解为什么在这种情况下人们可能更喜欢libc。这个问题的精神是试图理解和了解我在第一个代码中的错误所在,并对libc的内部机制有更深入的了解。@Phoenix87:这是个好问题,我只是对你的术语感到恼火。但如果你说一个进程查看器工具将其报告为僵尸的话,也许我应该自己尝试一下。我只是假设如果仍有线程在运行,它不会显示为僵尸,但如果初始PID已退出,它可能会显示为僵尸。(线程使用与PID相同的编号空间,即有自己的PID,但是getpid()返回初始线程的PID。因此“PID”线程的个数实际上是线程ID。请参阅/proc/*/task。另外,请注意,进程不必等待自己。外壳等待其所有子进程,因此直接从外壳运行的进程在退出时总是会立即收获。这就是为什么有一个僵尸进程的原因。对于普通的单线程进程,这种情况不会发生当您从bash运行这些程序时。@Phoenix87:BTW,您可以使用我上次编辑中的建议使
main()
更高效/更小。@Phoenix87:update:yes,
ps
将其报告为僵尸。我使用来自的输出更新了我的答案