Linux X86 程序启动 – main函数是如何被执行的? – 落木萧萧的博客 (luomuxiaoxiao.com)

 main函数是如何被执行的?

Contents

  • 一、目标读者
  • 二、覆盖范围
  • 三、调用过程分析
    • 3.1 main函数的调用
      • main函数如何被调用
    • 3.2 _start函数分析
      • 3.2.1 首先,_start是如何启动的?
      • 3.2.2 _start函数就是我们开始的地方
      • 3.2.3 调用__libc_start_main之前的设置
      • 3.2.4 环境变量哪里去了?
    • 3.3 __libc_start_main函数分析
      • 3.3.1 __libc_start_main功能概述
      • 3.3.2 调用init参数
    • 3.4 __libc_csu_init函数分析
      • 3.4.1 用户应用程序的构造函数
      • 3.4.2 这个函数到底是干什么的?
      • 3.4.3 但是__libc_csu_init里的循环是干什么的?
    • 3.5 _init函数分析
      • 3.5.1 init函数的调用
      • 3.5.2 _init函数起始于常规的C函数调用
    • 3.6 gmon_start函数分析
      • 生成profile文件
    • 3.7 frame_dummy函数分析
      • 函数并不是空的
    • 3.8 _do_global_ctors_aux函数分析
      • 3.8.1 终于到构造函数了!
      • 3.8.2 来看个例子
      • 3.8.3 prog2的_init函数,像极了prog1的
      • 3.8.4 这是将要调用的函数的源代码
      • 3.8.5 汇编语言也是这样
      • 3.8.6 函数开始的部分
      • 3.8.7 循环之前的设置
      • 3.8.8 此时执行到了loop的顶端
      • 3.8.9 函数谢幕
      • 3.8.10 承诺过你的使用debugger进入prog2
    • 3.9 回到__libc_csu_init__
    • 3.10 这是另一个函数的循环调用
    • 3.11 程序将返回__libc_start_main__
      • exit()函数运行了更多的循环
  • 四、这个程序,把上面所有的过程联系了起来
  • 五、结尾
  • 六、参考阅读

译者注: 本文是我在理解可执行文件代码段时,从网上搜索到的一篇文章,原文是英文的,我将其翻译成了中文。文章详细介绍了X86系统main函数调用前后的一些细节,并阐述了C程序的构造函数和析构函数,以及 .init,.fini,init_arrayfini_array各section相对于main函数及彼此的执行顺序。遗憾的是这篇文章是基于32位CPU架构来研究的,而本博客的文章是以64位CPU架构来研究的。如果有时间我会顺着相同的思路在64位机器上将该过程整理出来。其实两者仅在汇编语言传参方式和位置无关码的生成方式上略有区别,所以这篇文章还是有很大借鉴意义的。

如果你对文章内容或者翻译的内容有任何问题,欢迎在我博客这篇文章下留言讨论。

原文链接:Linux x86 Program Start Up

一、目标读者

这篇文章主要面向对象是为了那些想深入了解linux下程序的加载过程的读者,它主要介绍了X86 ELF文件的动态加载过程。这篇文章将会使你理解如何debug main函数启动前发生的问题。本文基于事实描述,但是将会忽略一些与上述主题无关的细节。如果你是静态编译的,一些细节将会与本文的描述不符,这篇文章并不会列举出这些差异。当你读完这篇文章,你将会对X86的main函数启动前后非常了解。

二、覆盖范围

callgraph

当你读完,你将会理解上图。

三、调用过程分析

3.1 main函数的调用

main函数如何被调用

我们将编译一个最简单的C程序——空的main函数,然后,查看其反汇编代码以理解程序是如何从启动开始调用到main函数。从反汇编代码中,我们发现程序是由一个_start函数最终调用main函数执行的。

int main()
{
}

将上述代码保存为prog1.c,首先要做的是使用下面的命令编译这个文件:

gcc -ggdb -o prog1 prog1.c

我们首先查看其反汇编代码,通过这个程序来查看关于程序启动的一些过程,然后再用GDB去调试比这个版本稍微复杂一点的程序prog2。下面将会列举objdump -d prog1的输出,但是并不会按照该命令原本的顺序列举,而是会按照输出内容执行的顺序来输出(你可以自己dump这个结果,比如使用命令objdump -d prog1 > prog1.dump,就能保存objdump的输出,然后使用你熟悉的编辑器打开并查看它)。(但是RPUVI——一个真正的程序员是使用VI的)。

3.2 _start函数分析

3.2.1 首先,_start是如何启动的?

当你执行一个程序的时候,shell或者GUI会调用execve(),它会执行linux系统调用execve()。如果你想了解关于execve()函数,你可以简单的在shell中输入man execve。这些帮助来自于man手册(包含了所有系统调用)的第二节。简而言之,系统会为你设置栈,并且将argcargvenvp压入栈中。文件描述符0,1和2(stdin, stdout和stderr)保留shell之前的设置。加载器会帮你完成重定位,调用你设置的预初始化函数。当所有搞定之后,控制权会传递给_start(),下面是使用objdump -d prog1输出的_start函数的内容:

3.2.2 _start函数就是我们开始的地方

080482e0 <_start>:
80482e0:       31 ed                   xor    %ebp,%ebp
80482e2:       5e                      pop    %esi
80482e3:       89 e1                   mov    %esp,%ecx
80482e5:       83 e4 f0                and    $0xfffffff0,%esp
80482e8:       50                      push   %eax
80482e9:       54                      push   %esp
80482ea:       52                      push   %edx
80482eb:       68 00 84 04 08          push   $0x8048400
80482f0:       68 a0 83 04 08          push   $0x80483a0
80482f5:       51                      push   %ecx
80482f6:       56                      push   %esi
80482f7:       68 94 83 04 08          push   $0x8048394
80482fc:       e8 c3 ff ff ff          call   80482c4 <__libc_start_main@plt>
8048301:       f4

任何值xor自身得到的结果都是0。所以xor %ebp,%ebp语句会把%ebp设置为0。ABI(Application Binary Interface specification)推荐这么做,目的是为了标记最外层函数的页帧(frame)。接下来,从栈中弹出栈顶的值保存到%esi。在最开始的时候我们把argcargvenvp放到了栈里,所以现在的pop语句会把argc放到%esi中。这里只是临时保存一下,稍后我们会把它再次压回栈中。因为我们弹出了argc,所以%ebp现在指向的是argvmov指令把argv放到了%ecx中,但是并没有移动栈指针。然后,将栈指针和一个可以清除后四位的掩码做and操作。根据当前栈指针的位置不同,栈指针将会向下移动0到15个字节。这么做,保证了任何情况下,栈指针都是16字节的偶数倍对齐的。对齐的目的是保证栈上所有的变量都能够被内存和cache快速的访问。要求这么做的是SSE,就是指令都能在单精度浮点数组上工作的那个(扩展指令集)。比如,某次运行时,_start函数刚被调用的时候,%esp处于0xbffff770。在我们从栈上弹出argc后,%esp指向0xbffff774。它向高地址移动了(往栈里存放数据,栈指针地址向下增长;从栈中取出数据,栈指针地址向上增长)。当对栈指针执行了and操作后,栈指针回到了0xbffff770

3.2.3 调用__libc_start_main之前的设置

现在,我们把__libc_start_main函数的参数压入栈中。第一个参数%eax被压入栈中,里面保存了无效信息,原因是稍后会有七个参数将被压入栈中,但是为了保证16字节对齐,所以需要第八个参数。这个值也并不会被用到。__libc_start_main是在链接的时候从glibc复制过来的。在glibc的代码中,它位于csu/libc-start.c文件里。__libc_start_main的定义如下:

int __libc_start_main(  int (*main) (int, char * *, char * *),int argc, char * * ubp_av,void (*init) (void),void (*fini) (void),void (*rtld_fini) (void),void (* stack_end));

所以,我们期望_start函数能够将__libc_start_main需要的参数按照逆序压入栈中。

libc_start_main

调用__libc_start_main函数前,栈的内容

__libc_csu_fini函数也是从glibc被链接进我们代码的,它的源代码位于csu/elf-init.c中。稍后我们会看到它。

3.2.4 环境变量哪里去了?

你是否注意到我们并没有获取envp(栈里指向我们环境变量的指针)?它并不是__libc_start_main函数的参数。但是我们知道main函数的原型其实是int main(int argc, char** argv, char** envp)。所以,到底怎么回事?

void __libc_init_first(int argc, char *arg0, ...)
{char **argv = &arg0, **envp = &argv[argc + 1];__environ = envp;__libc_init (argc, argv, envp);
}

其实,__libc_start_main函数会调用__libc_init_first,这个函数会使用内部信息去找到环境变量(实际上环境变量就位于argv的终止字符null的后面),然后设置一个全局变量__environ,这个全局变量可以被__libc_start_main函数内部任何地方使用,包括调用main函数时。当envp建立了之后,__libc_start_main函数会使用相同的小技巧,越过envp数组之后的NULL字符,获取另一个向量——ELF辅助向量(加载器使用它给进程传递一些信息)。通过一个简单的方法可以查看里面的内容:运行程序前,设置环境变量LD_SHOW_AUXV=1。这是对于prog1运行的结果。

$ LD_SHOW_AUXV=1 ./prog1
AT_SYSINFO:      0xe62414
AT_SYSINFO_EHDR: 0xe62000
AT_HWCAP:    fpu vme de pse tsc msr pae mce cx8 apicmtrr pge mca cmov pat pse36 clflush dtsacpi mmx fxsr sse sse2 ss ht tm pbe
AT_PAGESZ:       4096
AT_CLKTCK:       100
AT_PHDR:         0x8048034
AT_PHENT:        32
AT_PHNUM:        8
AT_BASE:         0x686000
AT_FLAGS:        0x0
AT_ENTRY:        0x80482e0
AT_UID:          1002
AT_EUID:         1002
AT_GID:          1000
AT_EGID:         1000
AT_SECURE:       0
AT_RANDOM:       0xbff09acb
AT_EXECFN:       ./prog1
AT_PLATFORM:     i686

有趣吧?各种各样的信息。AT_ENTRY_start的地址,还有我们的UID、有效UID和GID。而且,可以看出来我们的电脑是i686,times()的频率是100(每秒的clock-ticks数?稍后我调查一下)。AT_PHDR是ELF program header 的位置,它包括了程序中所有segment在内存中的位置信息,重定位条目和加载器需要的一些信息。AT_PHENT是header entry的字节数。接下来我们就不再顺着这个思路研究下去了,因为我们并不需要这些信息。

3.3 __libc_start_main函数分析

3.3.1 __libc_start_main功能概述

稍后本文会详细介绍__libc_start_main函数,但是,它的主要功能如下:

  1. 处理关于setuid、setgid程序的安全问题
  2. 启动线程
  3. fini函数和rtld_fini函数作为参数传递给at_exit调用,使它们在at_exit里被调用,从而完成用户程序和加载器的调用结束之后的清理工作
  4. 调用其init参数
  5. 调用main函数,并把argcargv参数、环境变量传递给它
  6. 调用exit函数,并将main函数的返回值传递给它

3.3.2 调用init参数

__libc_start_main函数的init参数被设置成了__libc_csu_init函数,它也是被链接进我们代码的。它来源于glibc源代码中的csu/elf-init.c。其C代码如下(原代码只不过多了一些#ifdef):

void
__libc_csu_init (int argc, char **argv, char **envp)
{_init ();const size_t size = __init_array_end - __init_array_start;for (size_t i = 0; i < size; i++)(*__init_array_start [i]) (argc, argv, envp);
}

3.4 __libc_csu_init函数分析

3.4.1 用户应用程序的构造函数

__libc_csu_init函数相当重要,因为它是我们可执行程序的构造函数。“等等!,我们的程序不是C++程序啊!”。是的,不是C++程序,但是构造函数和析构函数的概念并非属于C++,因为它的诞生早于C++。对于任意的可执行程序都可以有一个C函数的构造函数__libc_csu_init和C函数的析构函数__libc_csu_fini。在构造函数内部,你将会看到,可执行程序会找到全局C函数组成的构造函数集,并且调用它们。任何一个C程序都是可以有的构造函数集的。稍后,我会展示一下。如果你觉得别扭,你可以将它们称为InitializersFinalizers。下面是__libc_csu_init函数的反汇编代码:

080483a0 <__libc_csu_init>:80483a0:       55                      push   %ebp80483a1:       89 e5                   mov    %esp,%ebp80483a3:       57                      push   %edi80483a4:       56                      push   %esi80483a5:       53                      push   %ebx80483a6:       e8 5a 00 00 00          call   8048405 <__i686.get_pc_thunk.bx>80483ab:       81 c3 49 1c 00 00       add    $0x1c49,%ebx80483b1:       83 ec 1c                sub    $0x1c,%esp80483b4:       e8 bb fe ff ff          call   8048274 <_init>80483b9:       8d bb 20 ff ff ff       lea    -0xe0(%ebx),%edi80483bf:       8d 83 20 ff ff ff       lea    -0xe0(%ebx),%eax80483c5:       29 c7                   sub    %eax,%edi80483c7:       c1 ff 02                sar    $0x2,%edi80483ca:       85 ff                   test   %edi,%edi80483cc:       74 24                   je     80483f2 <__libc_csu_init+0x52>80483ce:       31 f6                   xor    %esi,%esi80483d0:       8b 45 10                mov    0x10(%ebp),%eax80483d3:       89 44 24 08             mov    %eax,0x8(%esp)80483d7:       8b 45 0c                mov    0xc(%ebp),%eax80483da:       89 44 24 04             mov    %eax,0x4(%esp)80483de:       8b 45 08                mov    0x8(%ebp),%eax80483e1:       89 04 24                mov    %eax,(%esp)80483e4:       ff 94 b3 20 ff ff ff    call   *-0xe0(%ebx,%esi,4)80483eb:       83 c6 01                add    $0x1,%esi80483ee:       39 fe                   cmp    %edi,%esi80483f0:       72 de                   jb     80483d0 <__libc_csu_init+0x30>80483f2:       83 c4 1c                add    $0x1c,%esp80483f5:       5b                      pop    %ebx80483f6:       5e                      pop    %esi80483f7:       5f                      pop    %edi80483f8:       5d                      pop    %ebp80483f9:       c3                      ret

3.4.2 这个函数到底是干什么的?

再这我们先不多说了,但是我觉得你还挺想知道的。get_pc_truck函数有点有趣。它是给位置无关码使用的。设置它们可以让位置无关码正常工作。为了让它们工作,基址寄存器(%ebp)需要知道GLOBAL_OFFSET_TABLE。其部分代码如下:

push %ebx
call __get_pc_thunk_bx
add  $_GLOBAL_OFFSET_TABLE_,%ebx
__get_pc_thunk_bx:
movel (%esp),%ebx
return

好,我们仔细看看发生了什么。调用__get_pc_thunk_bx时,像所有其他函数调用一样,将下一条指令的地址压入栈中。这样,当函数返回时,就会继续执行下条指令。这个地址就是我们需要的地址。所以,在__get_pc_thunk_bx中,我们将返回地址从栈中复制到%ebx中。当返回的时候,下条指令会把_GLOBAL_OFFSET_TABLE_加到%ebx上去,其中_GLOBAL_OFFSET_TABLE_代表了当前地址和位置无关码使用的GOT(global offset table)的差值。在GOT中保存了我们想访问的变量的指针的集合,并且我们只需要知道数据在这个表中的偏移量就行。加载器会为我们修改这个表里面的地址。对于函数来讲,也有一个类似的表(PLT)。汇编里面这么编写实在是太烦人了,但是,在C或者C++中,你可以将-pic参数传递给编译器,它将会自动帮你完成这个工作。如果有兴趣的话可以看看编译器的源码,你就知道编译器如何使用-pic这个标志去编译源码了。

译者注:上述·get_pc_truck·函数的主要目的其实是获取变量对应的GOT,以通过它获取变量真正的值。之所以这么写,是因为在32位系统里,没有类似于rip的寄存器,因此并不能直接获取当前指令的地址,而在64位系统里就不用这种小技巧了。详细请参考参考阅读[1].

3.4.3 但是__libc_csu_init里的循环是干什么的?

等我们讨论完init()(实际上调用的是_init)之后,我们将会讨论__libc_csu_init函数里的循环。现在我们只要记住:它调用了用户程序中所有用C代码编写的initializers.

3.5 _init函数分析

3.5.1 init函数的调用

好的,当加载器将控制权交给_start函数之后,_start函数将会调用__libc_start_main函数,__libc_start_main函数会调用__libc_csu_init函数, __libc_csu_init函数会调用_init函数。

08048274 <_init>:8048274:       55                      push   %ebp8048275:       89 e5                   mov    %esp,%ebp8048277:       53                      push   %ebx8048278:       83 ec 04                sub    $0x4,%esp804827b:       e8 00 00 00 00          call   8048280 <_init+0xc>8048280:       5b                      pop    %ebx8048281:       81 c3 74 1d 00 00       add    $0x1d74,%ebx        (.got.plt)8048287:       8b 93 fc ff ff ff       mov    -0x4(%ebx),%edx804828d:       85 d2                   test   %edx,%edx804828f:       74 05                   je     8048296 <_init+0x22>8048291:       e8 1e 00 00 00          call   80482b4 <__gmon_start__@plt>8048296:       e8 d5 00 00 00          call   8048370 <frame_dummy>804829b:       e8 70 01 00 00          call   8048410 <__do_global_ctors_aux>80482a0:       58                      pop    %eax80482a1:       5b                      pop    %ebx80482a2:       c9                      leave80482a3:       c3                      ret

3.5.2 _init函数起始于常规的C函数调用

如果你想了解C函数调用规范的话,请参考这篇博客Basic Assembler Debugging with GDB。简单来讲就是,调用者的基址寄存器(%ebp)会被保存到栈里,当前函数的基址寄存器(%ebp)会指向栈顶,然后,保留4个字节空间。这里有趣的是第一次函数调用。它的作用和我们之前看到的之前调用get_pc_trunk非常像。如果你仔细看的话,发现调用的是下一条指令的地址!这就好像仅仅是顺序执行了而已,但是这么做的目的是,当前的地址被压入了栈中。然后通过弹出栈操作,又把它放到了%ebx中,之后就可以用它来设置访问全局访问表了。

3.6 gmon_start函数分析

生成profile文件

然后,我们来看gmon_start函数。如果它是空的,我们跳过它,不调用它。否则,调用它来设置profiling。该函数调用一个例程开始profiling,并且调用at_exit去调用另一个程序运行,并且在运行结束的时候生成gmon.out。

译者注: 为了优化软件中频繁调用的部分,从而提高程序整体执行的效率,我们可以在使用gcc编译的时候加上 -pg标志。这样在程序运行结束的时候会生成一个记录程序运行状态的文件叫做gmon.out。然后,我们可以使用一个名为gprof的GNU profiler工具来分析该文件从而获得程序各部分的运行时间,来反映其运行性能。详情请参考参考阅读[2].

3.7 frame_dummy函数分析

函数并不是空的

完成上述两者之一的某个函数之后,接下来frame_dummy函数会被调用。其目的是调用__register_frame_info函数,但是,调用frame_dummy是为了给上述函数设置参数。这么做的目的是为了在出错时设置unwinding stack frames。这个非常有意思,但是并不是这次讨论的主题,所以以后有机会我们再讨论它。

3.8 _do_global_ctors_aux函数分析

3.8.1 终于到构造函数了!

终于调用到_do_global_ctors_aux函数了。如果在调用main函数之前,你的程序出了问题,你很可能需要看看这个函数。当然,这里存放了全局C++对象的构造函数,但是,这里也能存放其他东西。

3.8.2 来看个例子

我们修改程序prog1,并把它叫做prog2。令人兴奋的部分是__attribute__ ((constructor)),它告诉GCC:链接器应该在__do_global_ctors_aux使用的表里创建一个指针指向这里。如你所见,我们编写的构造函数确实运行了(__FUNCTION__被编译器替换成了当前函数的名字,这就是GCC魅力所在)。

#include <stdio.h>void __attribute__ ((constructor)) a_constructor() {printf("%s\n", __FUNCTION__);
}int
main()
{printf("%s\n",__FUNCTION__);
}
$ ./prog2
a_constructor
main
$

3.8.3 prog2的_init函数,像极了prog1的

稍后我们将使用GDB看看到底发生了什么。我们将进入prog2的_init函数。

08048290 <_init>:8048290:       55                      push   %ebp8048291:       89 e5                   mov    %esp,%ebp8048293:       53                      push   %ebx8048294:       83 ec 04                sub    $0x4,%esp8048297:       e8 00 00 00 00          call   804829c <_init+0xc>804829c:       5b                      pop    %ebx804829d:       81 c3 58 1d 00 00       add    $0x1d58,%ebx80482a3:       8b 93 fc ff ff ff       mov    -0x4(%ebx),%edx80482a9:       85 d2                   test   %edx,%edx80482ab:       74 05                   je     80482b2 <_init+0x22>80482ad:       e8 1e 00 00 00          call   80482d0 <__gmon_start__@plt>80482b2:       e8 d9 00 00 00          call   8048390 <frame_dummy>80482b7:       e8 94 01 00 00          call   8048450 <__do_global_ctors_aux>80482bc:       58                      pop    %eax80482bd:       5b                      pop    %ebx80482be:       c9                      leave80482bf:       c3                      ret

我们可以看到,上述的地址和prog1的地址略微有所不同。这些有差异的地址似乎相对于prog1移动了28个字节。这里,有两个函数:"a_constructor"(加上结束符一共14个字节)、"main"(加上结束符一共5个字节)和两个格式化字符串"%s\n"(2*4个字节,加上一个1字节的换行符和终止符),所以14 + 5 + 4 + 4 = 27? 似乎还差一个。不管怎样,这只是个猜想,我就不仔细研究了。然后我们就要跳入到__do_global_ctors_aux函数中去,看看到底发生了什么。

3.8.4 这是将要调用的函数的源代码

为了方便研究,我们列举出__do_global_ctors_aux函数的C代码,它位于GCC源码中的gcc/crtstuff.c里。

__do_global_ctors_aux (void)
{func_ptr *p;for (p = __CTOR_END__ - 1; *p != (func_ptr) -1; p--)(*p) ();
}

如上所示,p的值被初始化成__CTOR_END__减去一个字节。这是一种指针算法,如果指针指向一个函数,在这种情况下,-1表示向上移动一个指针或者说4个字节。我们也能从汇编里面看出来。当指针不等于-1时,调用这个指针指向的函数,并且再次将指针上移。很明显,这个指针数组起始于-1,并且包含若干个函数指针。

3.8.5 汇编语言也是这样

下面是使用objdump -d得到的__do_global_ctors_aux函数对应的汇编语言。我们将仔细的查看它的每条指令,以便你就能够在我们使用debugger之前完全了解它。

08048450 <__do_global_ctors_aux>:8048450:       55                      push   %ebp8048451:       89 e5                   mov    %esp,%ebp8048453:       53                      push   %ebx8048454:       83 ec 04                sub    $0x4,%esp8048457:       a1 14 9f 04 08          mov    0x8049f14,%eax804845c:       83 f8 ff                cmp    $0xffffffff,%eax804845f:       74 13                   je     8048474 <__do_global_ctors_aux+0x24>8048461:       bb 14 9f 04 08          mov    $0x8049f14,%ebx8048466:       66 90                   xchg   %ax,%ax8048468:       83 eb 04                sub    $0x4,%ebx804846b:       ff d0                   call   *%eax804846d:       8b 03                   mov    (%ebx),%eax804846f:       83 f8 ff                cmp    $0xffffffff,%eax8048472:       75 f4                   jne    8048468 <__do_global_ctors_aux+0x18>8048474:       83 c4 04                add    $0x4,%esp8048477:       5b                      pop    %ebx8048478:       5d                      pop    %ebp8048479:       c3                      ret

3.8.6 函数开始的部分

函数最开始的部分依然遵从了C函数正常的调用惯例(保存调用者的栈基址寄存器,设置当前函数的栈基址寄存器),本函数中还增加了一点:额外把%ebx保存到了栈中,因为这个函数后面会使用到它。同时,我们也为(C代码中的)指针p保留了空间。你可能注意到了,即使我们在栈上为其开辟了空间,但是从未使用这部分空间。取而代之的是,p将会保存到%ebx中,*p会保存到%eax中。

3.8.7 循环之前的设置

看起来编译器做了一些优化,编译器并没有直接“加载__CTOR_END__,然后将其值减去1,再查找它指向的内容”,而是直接加载*(__CTOR_END__ - 1),这是一个立即数0x8049f14(注意,$0x8049f14意思是一个立即数,而不带$,只写0x8049f14的意思是这个地址指向的内容)。这个数里面的内容被直接放到了%eax中,然后立刻比较%eax和-1,如果相等,则跳转到地址0x8048474,回收栈,弹出我们保存在栈里的内容,函数调用结束,返回。

假设在函数表中至少有一个值,立即数0x8049f14被存放到%ebx,也就是函数指针p,然后执行指令xchg %ax,%ax,这是什么鬼?原来这是X86 16或者32位里的一个nop(No Operation)语句。它什么也不做,只是占据了一个指令周期,起一个占位符作用而已。在这种情况下,使循环开始于8048468,而不是8048466。这么做的好处是使循环开始的地方以4字节对齐,这样整个循环将会极大可能的被保存到一个cache line里,而不会被分成两段,从而起到加速执行的作用。

3.8.8 此时执行到了loop的顶端

接下来,将%ebx减去4,从而为下一次循环做好准备,调用%eax里保存的地址对应的函数,然后将下一个函数指针移至%eax中,并且和-1比较,如果不等于-1,再次调回到上述循环。

3.8.9 函数谢幕

此时,已经运行到函数的最后,然后返回到_init中,然后又运行到_init函数的最后,并返回__libc_csu_init__中。你肯定已经忘了吧!此时仍然在循环处理中呢!但是首先我们完成之前的承诺。

3.8.10 承诺过你的使用debugger进入prog2

开始吧!需要记住一点的是:GDB总是显示你将要执行的下一行或者下一条指令

$ !gdb
gdb prog2
Reading symbols from /home/patrick/src/asm/prog2...done.
(gdb) set disassemble-next-line on
(gdb) b *0x80482b7
Breakpoint 1 at 0x80482b7

运行调试器,打开disassemble-next-line,这样它就会总是显示下一条将要执行的指令的汇编代码,然后我们在_init函数将要调用__do_global_ctors_aux函数的地方设置一个断点。

(gdb) r
Starting program: /home/patrick/src/asm/prog2 Breakpoint 1, 0x080482b7 in _init ()
=> 0x080482b7 <_init+39>:    e8 94 01 00 00 call   0x8048450 <__do_global_ctors_aux>
(gdb) si
0x08048450 in __do_global_ctors_aux ()
=> 0x08048450 <__do_global_ctors_aux+0>:     55 push   %ebp

输入r继续运行程序,到达断点处。再输入si单步执行指令,现在我们进入了__do_global_ctors_aux函数内部。后面你会看到若干次我并没输入任何指令,但是GDB却继续执行,这是因为我只是按了回车而已,GDB默认会重复上条指令。所以,如果我按下回车,GDB将会按照输入si继续执行。

(gdb)
0x08048451 in __do_global_ctors_aux ()
=> 0x08048451 <__do_global_ctors_aux+1>:     89 e5  mov    %esp,%ebp
(gdb) 
0x08048453 in __do_global_ctors_aux ()
=> 0x08048453 <__do_global_ctors_aux+3>:     53 push   %ebx
(gdb) 
0x08048454 in __do_global_ctors_aux ()
=> 0x08048454 <__do_global_ctors_aux+4>:     83 ec 04   sub    $0x4,%esp
(gdb) 
0x08048457 in __do_global_ctors_aux ()

好的,现在我们已经执行完程序最开始的部分,接下来将要执行真正的代码了。

(gdb)
=> 0x08048457 <__do_global_ctors_aux+7>:     a1 14 9f 04 08 mov    0x8049f14,%eax
(gdb) 
0x0804845c in __do_global_ctors_aux ()
=> 0x0804845c <__do_global_ctors_aux+12>:    83 f8 ff   cmp    $0xffffffff,%eax
(gdb) p/x $eax
$1 = 0x80483b4

我想知道加载完指针之后会是什么样,所以输入了p/x $eax,意思是以十六进制的形式打印寄存器%eax的内容。它不等于-1,所以我们假定程序将继续执行循环。现在由于我的最后一条指令是print指令,所以我不能按回车继续执行了,下次我就得输入si了。

(gdb) si
0x0804845f in __do_global_ctors_aux ()
=> 0x0804845f <__do_global_ctors_aux+15>:    74 13  je     0x8048474 <__do_global_ctors_aux+36>
(gdb) 
0x08048461 in __do_global_ctors_aux ()
=> 0x08048461 <__do_global_ctors_aux+17>:    bb 14 9f 04 08 mov    $0x8049f14,%ebx
(gdb) 
0x08048466 in __do_global_ctors_aux ()
=> 0x08048466 <__do_global_ctors_aux+22>:    66 90  xchg   %ax,%ax
(gdb) 
0x08048468 in __do_global_ctors_aux ()
=> 0x08048468 <__do_global_ctors_aux+24>:    83 eb 04   sub    $0x4,%ebx
(gdb) 
0x0804846b in __do_global_ctors_aux ()
=> 0x0804846b <__do_global_ctors_aux+27>:    ff d0  call   *%eax
(gdb) 
a_constructor () at prog2.c:3
3   void __attribute__ ((constructor)) a_constructor() {
=> 0x080483b4 <a_constructor+0>:     55 push   %ebp0x080483b5 <a_constructor+1>:     89 e5  mov    %esp,%ebp0x080483b7 <a_constructor+3>:     83 ec 18   sub    $0x18,%esp

这部分代码很有意思。我们一步步调用来看看。现在我们已经进入了我们自己写的函数a_constructor。因为GDB是能看到我们的源代码的,所以它在下一行给出了我们源码。又因为我打开了disassemble-next-line,所以它也会给出对应的汇编代码。这个例子中输出了函数最开始的部分,对应了函数的声明,所以我们得到了三行汇编。有意思吧?现在,我输入n命令,这个时候我们写的prinf就会被调用了。第一个n跳过了程序最开始的部分,第二个n执行prinf,第三个n执行了函数的结尾部分。如果你想知道为什么你需要在函数最开始和结束部分做些处理的话,现在,你使用GDB的单步调试应该能知道答案了吧。

(gdb) n
4       printf("%s\n", __FUNCTION__);
=> 0x080483ba <a_constructor+6>:     c7 04 24 a5 84 04 08   movl   $0x80484a5,(%esp)0x080483c1 <a_constructor+13>:    e8 2a ff ff ff call   0x80482f0 <puts@plt>

之前,我们已经把a_constructor字符串的地址作为printf的参数保存到了栈里,因为编译器足够的智能,发现实际上puts函数才是我们想要的,所以它调用了puts函数。

(gdb) n
a_constructor
5   }
=> 0x080483c6 <a_constructor+18>:    c9 leave  0x080483c7 <a_constructor+19>:    c3 ret   

因为我们正在运行中来调试程序,所以我们看到了a_constructor打印出了上面的内容。后括号}对应了函数的结尾部分,被显示出来了。提示一下,如果你不清楚leave指令的话,实际上它做了一下操作:

    movl %ebp, %esppopl %ebp

继续执行,我们就退出了函数,并返回了调用函数。这里我又不得不输入si了:

(gdb) n
0x0804846d in __do_global_ctors_aux ()
=> 0x0804846d <__do_global_ctors_aux+29>:    8b 03  mov    (%ebx),%eax
(gdb) si
0x0804846f in __do_global_ctors_aux ()
=> 0x0804846f <__do_global_ctors_aux+31>:    83 f8 ff   cmp    $0xffffffff,%eax
(gdb) 
0x08048472 in __do_global_ctors_aux ()
=> 0x08048472 <__do_global_ctors_aux+34>:    75 f4  jne    0x8048468 <__do_global_ctors_aux+24>
(gdb) p/x $eax
$2 = 0xffffffff

我比较好奇,并且再次看了一下:这次,我们的函数指针指向了-1,所以,程序退出了循环。

(gdb) si
0x08048474 in __do_global_ctors_aux ()
=> 0x08048474 <__do_global_ctors_aux+36>:    83 c4 04   add    $0x4,%esp
(gdb) 
0x08048477 in __do_global_ctors_aux ()
=> 0x08048477 <__do_global_ctors_aux+39>:    5b pop    %ebx
(gdb) 
0x08048478 in __do_global_ctors_aux ()
=> 0x08048478 <__do_global_ctors_aux+40>:    5d pop    %ebp
(gdb) 
0x08048479 in __do_global_ctors_aux ()
=> 0x08048479 <__do_global_ctors_aux+41>:    c3 ret    
(gdb) 
0x080482bc in _init ()
=> 0x080482bc <_init+44>:    58 pop    %eax

注意,我们现在退回到了_init

(gdb) 
0x080482bd in _init ()
=> 0x080482bd <_init+45>:    5b pop    %ebx
(gdb) 
0x080482be in _init ()
=> 0x080482be <_init+46>:    c9 leave  
(gdb) 
0x080482bf in _init ()
=> 0x080482bf <_init+47>:    c3 ret    
(gdb) 
0x080483f9 in __libc_csu_init ()
=> 0x080483f9 <__libc_csu_init+25>:  8d bb 1c ff ff ff  lea    -0xe4(%ebx),%edi
(gdb) q
A debugging session is active.Inferior 1 [process 17368] will be killed.Quit anyway? (y or n) y
$

现在,程序跳转回__libc_csu_init函数,然后我们输入q退出了调试器。以上是我之前说的调试过程。现在我们回到__libc_csu_init__函数,这里还有另外一个循环要处理,我就不再进入循环单步分析了,但是我会概述一下。

3.9 回到__libc_csu_init__

我们刚刚经历了冗长的时间来分析一个汇编语言写的循环,这个用汇编写的循环要比上一个更加复杂。所以我留给读者自行分析。这里我贴出对应的C代码:

void
__libc_csu_init (int argc, char **argv, char **envp)
{_init ();const size_t size = __init_array_end - __init_array_start;for (size_t i = 0; i < size; i++)(*__init_array_start [i]) (argc, argv, envp);
}

3.10 这是另一个函数的循环调用

__init__数组里面是什么呢?你肯定不会想到。你也可以在这个阶段自定义代码。这时刚刚从运行我们自定义的构造函数的_init函数返回,这意味着,在这个数组里面的内容将会在构造函数完成之后运行。你能通过某种方式告诉编译器你想在这个阶段运行某个你自定义的函数。这个函数也会收到和main函数相同的参数。

void init(int argc, char **argv, char **envp) {printf("%s\n", __FUNCTION__);
}__attribute__((section(".init_array"))) typeof(init) *__init = init;

我们并不这么做,因为这和之前的动作基本差不多。现在,我们返回到__lib_csu_init函数中,你还记得会返回到哪里吗?

3.11 程序将返回__libc_start_main__

它调用了我们的main函数,然后把main函数的返回值传递给exit()函数。

exit()函数运行了更多的循环

exit()函数按照注册顺序依次运行了在at_exit()中注册的函数。然后会运行另外一个循环,这次的循环是在__fini_数组中定义的。在运行完这些函数之后,就会调用析构函数。如下所示:

四、这个程序,把上面所有的过程联系了起来

#include <stdio.h>void preinit(int argc, char **argv, char **envp) {printf("%s\n", __FUNCTION__);
}void init(int argc, char **argv, char **envp) {printf("%s\n", __FUNCTION__);
}void fini() {printf("%s\n", __FUNCTION__);
}__attribute__((section(".init_array"))) typeof(init) *__init = init;
__attribute__((section(".preinit_array"))) typeof(preinit) *__preinit = preinit;
__attribute__((section(".fini_array"))) typeof(fini) *__fini = fini;void  __attribute__ ((constructor)) constructor() {printf("%s\n", __FUNCTION__);
}void __attribute__ ((destructor)) destructor() {printf("%s\n", __FUNCTION__);
}void my_atexit() {printf("%s\n", __FUNCTION__);
}void my_atexit2() {printf("%s\n", __FUNCTION__);
}int main() {atexit(my_atexit);atexit(my_atexit2);
}

编译并运行这个函数(这里我将其命名为hooks.c),输出如下:

$ ./hooks
preinit
constructor
init
my_atexit2
my_atexit
fini
destructor
$

五、结尾

现在我们再来回顾一下整个过程,这次你就不会对它感到陌生了吧。

callgraph

六、参考阅读

  1. Linux中的GOT和PLT到底是个啥?
  2. 使用 GNU profiler 来提高代码运行速度
查看全文
如若内容造成侵权/违法违规/事实不符,请联系编程学习网邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!

相关文章

  1. AudFree Auditior for Mac如何将 M4P 音乐和 AA/AAX 有声读物转换为 MP3 文件?

    AudFree Auditior 是一款功能强大的音频转换软件&#xff0c;可以轻松地将音频转换为 MP3、FLAC、AAC 等。那么如何使用AudFree Auditior for Mac将 M4P 音乐和 AA/AAX 有声读物转换为 MP3 文件&#xff1f;这里准备了教程&#xff0c;赶紧看看吧&#xff01; AudFree Auditio…...

    2024/4/26 15:57:11
  2. SQL语句大全实例

    SQL语句实例 表操作 例 1 对于表的教学管理数据库中的表 STUDENTS &#xff0c;可以定义如下&#xff1a; CREATE TABLE STUDENTS (SNO NUMERIC (6, 0) NOT NULL SNAME CHAR (8) NOT NULL AGE NUMERIC(3,0) SEX CHAR(2) BPLACE CHAR(20) PRIMARY KEY(SNO…...

    2024/4/26 16:32:14
  3. 【论文阅读】Super Odometry: IMU-centric LiDAR-Visual-Inertial Estimator for Challenging Environments

    Super Odometry: IMU-centric LiDAR-Visual-Inertial Estimator for Challenging Environments摘要I. 介绍II. 相关工作A. 松耦合激光-视觉-惯性里程计B. 紧耦合激光-视觉-惯性里程计C. 方法要点III.系统概述IV. 方法论A. IMU里程计因子1) IMU预积分因子:2) IMU里程计优化:B.激…...

    2024/4/18 14:44:59
  4. spring全局异常处理

    先来看下没加全局异常,发生报错的返回 简短的 长一点的 这样报错抛给前端会是一大堆,不方便处理 gatway的全局异常处理只能捕获模块调用间的异常,单独的模块报错还是要用到 ControllerAdvice 上核心代码 package com.zhzh.config.excep;import com.zhzh.model.ResultVO; i…...

    2024/4/20 17:33:44
  5. 学习笔记:JS遍历数组的三种写法

    方法一&#xff1a;forEach()方法 var 四大名著 [红楼梦, 西游记, 三国演义, 水浒传]; 四大名著.forEach(function(value, index, obj) { console.log("[" index "] " value); }) 方法二&#xff1a;for...in...方法 var 水浒传 [宋江, 李逵, 武…...

    2024/4/20 14:59:20
  6. Swift-技巧(三)使用元组(tuple)

    最近看 iOS 的官方功能的 Demo 时&#xff0c;发现代码中使用元组的地方很多&#xff0c;所以兴趣上来&#xff0c;查了下元组的出处。 在苹果的文档中就只有简短的两句&#xff0c;使用元组创建一个组合的值&#xff0c;从函数中返回多个值。元组中的可以使用属性值或者索引引…...

    2024/4/25 6:04:27
  7. js, 对已有对象,添加新属性值

    var it{ name: ‘yoyo’ , } console.log(it) it[“device”] { id: 123 } it[“serverType”] “1”; console.log(it)...

    2024/4/20 19:02:14
  8. 【攻防世界_web_10】webshell

    webshell 题目 小宁百度了php一句话,觉着很有意思,并且把它放在index.php里。 解题 直接尝试使用蚁剑链接 成功得到flag...

    2024/4/20 17:46:58
  9. org.hibernate.tool.schema.spi.CommandAcceptanceException: Error executing DDL异常问题定位

    原因&#xff1a;mysql不区分大小写&#xff0c;但是hibernate区分大小写造成的。...

    2024/4/23 15:31:55
  10. 中国风水墨风通用 (2)PPT模板

    模板介绍 精美PPT模板设计&#xff0c;中国风水墨风通用 (2)PPT模板。一套计划总结幻灯片模板&#xff0c;内含灰色,红色多种配色&#xff0c;精美水彩,复古,中国风风格设计&#xff0c;动态播放效果&#xff0c;精美实用。 一份设计精美的PPT模板&#xff0c;可以让你在汇报…...

    2024/4/24 12:17:42
  11. 什么是机器学习

    机器学习&#xff1a;涉及多方面的领域&#xff0c;是对计算机的算法进行研究&#xff0c;从而改善算法性能。...

    2024/4/25 23:08:30
  12. SpringBoot 静态获取 bean 的三种方式

    注意&#xff1a;调用者要被spring管理 1 目录 方式一 注解PostConstruct 方式二 启动类ApplicationContext 方式三 手动注入ApplicationContext 方式一 注解PostConstruct /*** springboot静态方法获取 bean 的三种方式(一)* author: clx* version: 1.1.0*/ Component …...

    2024/4/19 9:05:54
  13. 微软研究院开源DialoGPT:你有什么梦想?「让世界充满机器人」

    作者:Yizhe Zhang, Siqi Sun, Michel Galley等自然语言对话生成是人工智能社区面临的一大难题,微软研究院的一项新研究让我们离解决这一难题又更近了一步。他们用 GPT-2 模型——DialoGPT,在大规模 reddit 数据上预训练了一个对话系统,在多个对话数据集上取得了最佳结果。并…...

    2024/4/24 11:28:24
  14. jQuery谷歌日期和时间范围日历插件

    下载地址 基本的HTML to 引用js文件 JS调用 dd:...

    2024/4/19 2:28:35
  15. 领英精灵安全吗?附LinkedIn(领英)开发客户的关键点

    面对全球最大的职场社交平台——LinkedIn(领英)&#xff0c;很多蠢蠢欲动的外贸新人经常会问到&#xff1a;为什么那么多的外贸培训课程&#xff0c;机构和讲师以及一些外贸老鸟都会提到“如果想要把LinkedIn(领英)经营成一个有效的客户开发渠道&#xff0c;都会建议利用针对Li…...

    2024/4/25 14:51:04
  16. 转发这条DTRO反渗透膜应用电厂节能减排技术 火爆你的朋友圈

    我国人口众多&#xff0c;对电能的需求也是日益增多。在电能结构中&#xff0c;有将近70%都是煤炭&#xff0c;能耗量已经超过了世界平均能耗量&#xff0c;甚至比一些发达国家的能耗量还要高。电厂在运转中依靠水作为传递能量的介质&#xff0c;也是依靠水作为冷却介质来完成热…...

    2024/4/20 13:31:43
  17. 2021-2027全球与中国气溶胶采样器市场现状及未来发展趋势

    本报告研究全球与中国市场气溶胶采样器的产能、产量、销量、销售额、价格及未来趋势。重点分析全球与中国市场的主要厂商产品特点、产品规格、价格、销量、销售收入及全球和中国市场主要生产商的市场份额。历史数据为2016至2020年&#xff0c;预测数据为2021至2027年。 主要生产…...

    2024/4/26 13:19:49
  18. 【攻防世界_web_9】xff_referer

    xff_referer 题目 X老师告诉小宁其实xff和referer是可以伪造的。 解题 使用burpsuite抓包如图修改 不知道什么是X-Forwarded-For&#xff08;点这里&#xff09; 题目要求必须来自http:s://www.google.com 如图修改即可 不知道什么是Referer&#xff08;点这里&#xf…...

    2024/4/24 19:15:06
  19. Elasticsearch(三)使用 Kibana 操作 ES

    ...

    2024/4/25 5:55:37
  20. 策划经验分享——常用软件篇

    我作为一名主要工作为活动策划&#xff0c;并且已经工作了将近10年的社畜&#xff0c;对于活动策划可谓有很多想要吐槽的点&#xff0c;也有较多的经验&#xff0c;我的吐槽已经在上一篇吐的差不多了&#xff0c;现在就来跟大家唠一唠我的一些关于软件的经验推荐吧。 1.亿图脑…...

    2024/4/23 14:59:05

最新文章

  1. Spring框架宝典:彻底理解三级缓存策略

    一、循环依赖概念 在Spring应用中&#xff0c;循环依赖指的是两个或多个Bean之间相互引用&#xff0c;造成了一个环状的依赖关系。举例来说&#xff0c;如果Bean A依赖于Bean B&#xff0c;同时Bean B也依赖于Bean A&#xff0c;就形成了循环依赖。这种情况下&#xff0c;Sprin…...

    2024/4/28 21:02:53
  2. 梯度消失和梯度爆炸的一些处理方法

    在这里是记录一下梯度消失或梯度爆炸的一些处理技巧。全当学习总结了如有错误还请留言&#xff0c;在此感激不尽。 权重和梯度的更新公式如下&#xff1a; w w − η ⋅ ∇ w w w - \eta \cdot \nabla w ww−η⋅∇w 个人通俗的理解梯度消失就是网络模型在反向求导的时候出…...

    2024/3/20 10:50:27
  3. YOLOv9架构图分享

    YOLOv9是YOLO (You Only Look Once)系列实时目标检测系统的最新迭代。它建立在以前的版本之上&#xff0c;结合了深度学习技术和架构设计的进步&#xff0c;以在目标检测任务中实现卓越的性能。通过将可编程梯度信息(PGI)概念与广义ELAN (GELAN)架构相结合&#xff0c;YOLOv9在…...

    2024/4/25 10:53:55
  4. 零代码编程:用kimichat将PDF自动批量分割成多个图片

    有一个PDF文件&#xff0c;现在想把pdf文件转换成图片&#xff0c; 可以在kimichat中输入提示词&#xff1a; 你是一个Python编程专家&#xff0c;要完成一个将PDF文件自动批量分割成多个图片的任务&#xff0c;具体步骤如下&#xff1a; 打开d盘下的pdf文件&#xff1a;Ill …...

    2024/4/22 5:38:14
  5. 【外汇早评】美通胀数据走低,美元调整

    原标题:【外汇早评】美通胀数据走低,美元调整昨日美国方面公布了新一期的核心PCE物价指数数据,同比增长1.6%,低于前值和预期值的1.7%,距离美联储的通胀目标2%继续走低,通胀压力较低,且此前美国一季度GDP初值中的消费部分下滑明显,因此市场对美联储后续更可能降息的政策…...

    2024/4/28 13:52:11
  6. 【原油贵金属周评】原油多头拥挤,价格调整

    原标题:【原油贵金属周评】原油多头拥挤,价格调整本周国际劳动节,我们喜迎四天假期,但是整个金融市场确实流动性充沛,大事频发,各个商品波动剧烈。美国方面,在本周四凌晨公布5月份的利率决议和新闻发布会,维持联邦基金利率在2.25%-2.50%不变,符合市场预期。同时美联储…...

    2024/4/28 3:28:32
  7. 【外汇周评】靓丽非农不及疲软通胀影响

    原标题:【外汇周评】靓丽非农不及疲软通胀影响在刚结束的周五,美国方面公布了新一期的非农就业数据,大幅好于前值和预期,新增就业重新回到20万以上。具体数据: 美国4月非农就业人口变动 26.3万人,预期 19万人,前值 19.6万人。 美国4月失业率 3.6%,预期 3.8%,前值 3…...

    2024/4/26 23:05:52
  8. 【原油贵金属早评】库存继续增加,油价收跌

    原标题:【原油贵金属早评】库存继续增加,油价收跌周三清晨公布美国当周API原油库存数据,上周原油库存增加281万桶至4.692亿桶,增幅超过预期的74.4万桶。且有消息人士称,沙特阿美据悉将于6月向亚洲炼油厂额外出售更多原油,印度炼油商预计将每日获得至多20万桶的额外原油供…...

    2024/4/28 13:51:37
  9. 【外汇早评】日本央行会议纪要不改日元强势

    原标题:【外汇早评】日本央行会议纪要不改日元强势近两日日元大幅走强与近期市场风险情绪上升,避险资金回流日元有关,也与前一段时间的美日贸易谈判给日本缓冲期,日本方面对汇率问题也避免继续贬值有关。虽然今日早间日本央行公布的利率会议纪要仍然是支持宽松政策,但这符…...

    2024/4/27 17:58:04
  10. 【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响

    原标题:【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响近日伊朗局势升温,导致市场担忧影响原油供给,油价试图反弹。此时OPEC表态稳定市场。据消息人士透露,沙特6月石油出口料将低于700万桶/日,沙特已经收到石油消费国提出的6月份扩大出口的“适度要求”,沙特将满…...

    2024/4/27 14:22:49
  11. 【外汇早评】美欲与伊朗重谈协议

    原标题:【外汇早评】美欲与伊朗重谈协议美国对伊朗的制裁遭到伊朗的抗议,昨日伊朗方面提出将部分退出伊核协议。而此行为又遭到欧洲方面对伊朗的谴责和警告,伊朗外长昨日回应称,欧洲国家履行它们的义务,伊核协议就能保证存续。据传闻伊朗的导弹已经对准了以色列和美国的航…...

    2024/4/28 1:28:33
  12. 【原油贵金属早评】波动率飙升,市场情绪动荡

    原标题:【原油贵金属早评】波动率飙升,市场情绪动荡因中美贸易谈判不安情绪影响,金融市场各资产品种出现明显的波动。随着美国与中方开启第十一轮谈判之际,美国按照既定计划向中国2000亿商品征收25%的关税,市场情绪有所平复,已经开始接受这一事实。虽然波动率-恐慌指数VI…...

    2024/4/28 15:57:13
  13. 【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试

    原标题:【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试美国和伊朗的局势继续升温,市场风险情绪上升,避险黄金有向上突破阻力的迹象。原油方面稍显平稳,近期美国和OPEC加大供给及市场需求回落的影响,伊朗局势并未推升油价走强。近期中美贸易谈判摩擦再度升级,美国对中…...

    2024/4/27 17:59:30
  14. 【原油贵金属早评】市场情绪继续恶化,黄金上破

    原标题:【原油贵金属早评】市场情绪继续恶化,黄金上破周初中国针对于美国加征关税的进行的反制措施引发市场情绪的大幅波动,人民币汇率出现大幅的贬值动能,金融市场受到非常明显的冲击。尤其是波动率起来之后,对于股市的表现尤其不安。隔夜美国股市出现明显的下行走势,这…...

    2024/4/25 18:39:16
  15. 【外汇早评】美伊僵持,风险情绪继续升温

    原标题:【外汇早评】美伊僵持,风险情绪继续升温昨日沙特两艘油轮再次发生爆炸事件,导致波斯湾局势进一步恶化,市场担忧美伊可能会出现摩擦生火,避险品种获得支撑,黄金和日元大幅走强。美指受中美贸易问题影响而在低位震荡。继5月12日,四艘商船在阿联酋领海附近的阿曼湾、…...

    2024/4/28 1:34:08
  16. 【原油贵金属早评】贸易冲突导致需求低迷,油价弱势

    原标题:【原油贵金属早评】贸易冲突导致需求低迷,油价弱势近日虽然伊朗局势升温,中东地区几起油船被袭击事件影响,但油价并未走高,而是出于调整结构中。由于市场预期局势失控的可能性较低,而中美贸易问题导致的全球经济衰退风险更大,需求会持续低迷,因此油价调整压力较…...

    2024/4/26 19:03:37
  17. 氧生福地 玩美北湖(上)——为时光守候两千年

    原标题:氧生福地 玩美北湖(上)——为时光守候两千年一次说走就走的旅行,只有一张高铁票的距离~ 所以,湖南郴州,我来了~ 从广州南站出发,一个半小时就到达郴州西站了。在动车上,同时改票的南风兄和我居然被分到了一个车厢,所以一路非常愉快地聊了过来。 挺好,最起…...

    2024/4/28 1:22:35
  18. 氧生福地 玩美北湖(中)——永春梯田里的美与鲜

    原标题:氧生福地 玩美北湖(中)——永春梯田里的美与鲜一觉醒来,因为大家太爱“美”照,在柳毅山庄去寻找龙女而错过了早餐时间。近十点,向导坏坏还是带着饥肠辘辘的我们去吃郴州最富有盛名的“鱼头粉”。说这是“十二分推荐”,到郴州必吃的美食之一。 哇塞!那个味美香甜…...

    2024/4/25 18:39:14
  19. 氧生福地 玩美北湖(下)——奔跑吧骚年!

    原标题:氧生福地 玩美北湖(下)——奔跑吧骚年!让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 啊……啊……啊 两…...

    2024/4/26 23:04:58
  20. 扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!

    原标题:扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!扒开伪装医用面膜,翻六倍价格宰客!当行业里的某一品项火爆了,就会有很多商家蹭热度,装逼忽悠,最近火爆朋友圈的医用面膜,被沾上了污点,到底怎么回事呢? “比普通面膜安全、效果好!痘痘、痘印、敏感肌都能用…...

    2024/4/27 23:24:42
  21. 「发现」铁皮石斛仙草之神奇功效用于医用面膜

    原标题:「发现」铁皮石斛仙草之神奇功效用于医用面膜丽彦妆铁皮石斛医用面膜|石斛多糖无菌修护补水贴19大优势: 1、铁皮石斛:自唐宋以来,一直被列为皇室贡品,铁皮石斛生于海拔1600米的悬崖峭壁之上,繁殖力差,产量极低,所以古代仅供皇室、贵族享用 2、铁皮石斛自古民间…...

    2024/4/28 5:48:52
  22. 丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者

    原标题:丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者【公司简介】 广州华彬企业隶属香港华彬集团有限公司,专注美业21年,其旗下品牌: 「圣茵美」私密荷尔蒙抗衰,产后修复 「圣仪轩」私密荷尔蒙抗衰,产后修复 「花茵莳」私密荷尔蒙抗衰,产后修复 「丽彦妆」专注医学护…...

    2024/4/26 19:46:12
  23. 广州械字号面膜生产厂家OEM/ODM4项须知!

    原标题:广州械字号面膜生产厂家OEM/ODM4项须知!广州械字号面膜生产厂家OEM/ODM流程及注意事项解读: 械字号医用面膜,其实在我国并没有严格的定义,通常我们说的医美面膜指的应该是一种「医用敷料」,也就是说,医用面膜其实算作「医疗器械」的一种,又称「医用冷敷贴」。 …...

    2024/4/27 11:43:08
  24. 械字号医用眼膜缓解用眼过度到底有无作用?

    原标题:械字号医用眼膜缓解用眼过度到底有无作用?医用眼膜/械字号眼膜/医用冷敷眼贴 凝胶层为亲水高分子材料,含70%以上的水分。体表皮肤温度传导到本产品的凝胶层,热量被凝胶内水分子吸收,通过水分的蒸发带走大量的热量,可迅速地降低体表皮肤局部温度,减轻局部皮肤的灼…...

    2024/4/27 8:32:30
  25. 配置失败还原请勿关闭计算机,电脑开机屏幕上面显示,配置失败还原更改 请勿关闭计算机 开不了机 这个问题怎么办...

    解析如下&#xff1a;1、长按电脑电源键直至关机&#xff0c;然后再按一次电源健重启电脑&#xff0c;按F8健进入安全模式2、安全模式下进入Windows系统桌面后&#xff0c;按住“winR”打开运行窗口&#xff0c;输入“services.msc”打开服务设置3、在服务界面&#xff0c;选中…...

    2022/11/19 21:17:18
  26. 错误使用 reshape要执行 RESHAPE,请勿更改元素数目。

    %读入6幅图像&#xff08;每一幅图像的大小是564*564&#xff09; f1 imread(WashingtonDC_Band1_564.tif); subplot(3,2,1),imshow(f1); f2 imread(WashingtonDC_Band2_564.tif); subplot(3,2,2),imshow(f2); f3 imread(WashingtonDC_Band3_564.tif); subplot(3,2,3),imsho…...

    2022/11/19 21:17:16
  27. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机...

    win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”问题的解决方法在win7系统关机时如果有升级系统的或者其他需要会直接进入一个 等待界面&#xff0c;在等待界面中我们需要等待操作结束才能关机&#xff0c;虽然这比较麻烦&#xff0c;但是对系统进行配置和升级…...

    2022/11/19 21:17:15
  28. 台式电脑显示配置100%请勿关闭计算机,“准备配置windows 请勿关闭计算机”的解决方法...

    有不少用户在重装Win7系统或更新系统后会遇到“准备配置windows&#xff0c;请勿关闭计算机”的提示&#xff0c;要过很久才能进入系统&#xff0c;有的用户甚至几个小时也无法进入&#xff0c;下面就教大家这个问题的解决方法。第一种方法&#xff1a;我们首先在左下角的“开始…...

    2022/11/19 21:17:14
  29. win7 正在配置 请勿关闭计算机,怎么办Win7开机显示正在配置Windows Update请勿关机...

    置信有很多用户都跟小编一样遇到过这样的问题&#xff0c;电脑时发现开机屏幕显现“正在配置Windows Update&#xff0c;请勿关机”(如下图所示)&#xff0c;而且还需求等大约5分钟才干进入系统。这是怎样回事呢&#xff1f;一切都是正常操作的&#xff0c;为什么开时机呈现“正…...

    2022/11/19 21:17:13
  30. 准备配置windows 请勿关闭计算机 蓝屏,Win7开机总是出现提示“配置Windows请勿关机”...

    Win7系统开机启动时总是出现“配置Windows请勿关机”的提示&#xff0c;没过几秒后电脑自动重启&#xff0c;每次开机都这样无法进入系统&#xff0c;此时碰到这种现象的用户就可以使用以下5种方法解决问题。方法一&#xff1a;开机按下F8&#xff0c;在出现的Windows高级启动选…...

    2022/11/19 21:17:12
  31. 准备windows请勿关闭计算机要多久,windows10系统提示正在准备windows请勿关闭计算机怎么办...

    有不少windows10系统用户反映说碰到这样一个情况&#xff0c;就是电脑提示正在准备windows请勿关闭计算机&#xff0c;碰到这样的问题该怎么解决呢&#xff0c;现在小编就给大家分享一下windows10系统提示正在准备windows请勿关闭计算机的具体第一种方法&#xff1a;1、2、依次…...

    2022/11/19 21:17:11
  32. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”的解决方法...

    今天和大家分享一下win7系统重装了Win7旗舰版系统后&#xff0c;每次关机的时候桌面上都会显示一个“配置Windows Update的界面&#xff0c;提示请勿关闭计算机”&#xff0c;每次停留好几分钟才能正常关机&#xff0c;导致什么情况引起的呢&#xff1f;出现配置Windows Update…...

    2022/11/19 21:17:10
  33. 电脑桌面一直是清理请关闭计算机,windows7一直卡在清理 请勿关闭计算机-win7清理请勿关机,win7配置更新35%不动...

    只能是等着&#xff0c;别无他法。说是卡着如果你看硬盘灯应该在读写。如果从 Win 10 无法正常回滚&#xff0c;只能是考虑备份数据后重装系统了。解决来方案一&#xff1a;管理员运行cmd&#xff1a;net stop WuAuServcd %windir%ren SoftwareDistribution SDoldnet start WuA…...

    2022/11/19 21:17:09
  34. 计算机配置更新不起,电脑提示“配置Windows Update请勿关闭计算机”怎么办?

    原标题&#xff1a;电脑提示“配置Windows Update请勿关闭计算机”怎么办&#xff1f;win7系统中在开机与关闭的时候总是显示“配置windows update请勿关闭计算机”相信有不少朋友都曾遇到过一次两次还能忍但经常遇到就叫人感到心烦了遇到这种问题怎么办呢&#xff1f;一般的方…...

    2022/11/19 21:17:08
  35. 计算机正在配置无法关机,关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机...

    关机提示 windows7 正在配置windows 请勿关闭计算机 &#xff0c;然后等了一晚上也没有关掉。现在电脑无法正常关机以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;关机提示 windows7 正在配…...

    2022/11/19 21:17:05
  36. 钉钉提示请勿通过开发者调试模式_钉钉请勿通过开发者调试模式是真的吗好不好用...

    钉钉请勿通过开发者调试模式是真的吗好不好用 更新时间:2020-04-20 22:24:19 浏览次数:729次 区域: 南阳 > 卧龙 列举网提醒您:为保障您的权益,请不要提前支付任何费用! 虚拟位置外设器!!轨迹模拟&虚拟位置外设神器 专业用于:钉钉,外勤365,红圈通,企业微信和…...

    2022/11/19 21:17:05
  37. 配置失败还原请勿关闭计算机怎么办,win7系统出现“配置windows update失败 还原更改 请勿关闭计算机”,长时间没反应,无法进入系统的解决方案...

    前几天班里有位学生电脑(windows 7系统)出问题了&#xff0c;具体表现是开机时一直停留在“配置windows update失败 还原更改 请勿关闭计算机”这个界面&#xff0c;长时间没反应&#xff0c;无法进入系统。这个问题原来帮其他同学也解决过&#xff0c;网上搜了不少资料&#x…...

    2022/11/19 21:17:04
  38. 一个电脑无法关闭计算机你应该怎么办,电脑显示“清理请勿关闭计算机”怎么办?...

    本文为你提供了3个有效解决电脑显示“清理请勿关闭计算机”问题的方法&#xff0c;并在最后教给你1种保护系统安全的好方法&#xff0c;一起来看看&#xff01;电脑出现“清理请勿关闭计算机”在Windows 7(SP1)和Windows Server 2008 R2 SP1中&#xff0c;添加了1个新功能在“磁…...

    2022/11/19 21:17:03
  39. 请勿关闭计算机还原更改要多久,电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机怎么办...

    许多用户在长期不使用电脑的时候&#xff0c;开启电脑发现电脑显示&#xff1a;配置windows更新失败&#xff0c;正在还原更改&#xff0c;请勿关闭计算机。。.这要怎么办呢&#xff1f;下面小编就带着大家一起看看吧&#xff01;如果能够正常进入系统&#xff0c;建议您暂时移…...

    2022/11/19 21:17:02
  40. 还原更改请勿关闭计算机 要多久,配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以...

    配置windows update失败 还原更改 请勿关闭计算机&#xff0c;电脑开机后一直显示以以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;配置windows update失败 还原更改 请勿关闭计算机&#x…...

    2022/11/19 21:17:01
  41. 电脑配置中请勿关闭计算机怎么办,准备配置windows请勿关闭计算机一直显示怎么办【图解】...

    不知道大家有没有遇到过这样的一个问题&#xff0c;就是我们的win7系统在关机的时候&#xff0c;总是喜欢显示“准备配置windows&#xff0c;请勿关机”这样的一个页面&#xff0c;没有什么大碍&#xff0c;但是如果一直等着的话就要两个小时甚至更久都关不了机&#xff0c;非常…...

    2022/11/19 21:17:00
  42. 正在准备配置请勿关闭计算机,正在准备配置windows请勿关闭计算机时间长了解决教程...

    当电脑出现正在准备配置windows请勿关闭计算机时&#xff0c;一般是您正对windows进行升级&#xff0c;但是这个要是长时间没有反应&#xff0c;我们不能再傻等下去了。可能是电脑出了别的问题了&#xff0c;来看看教程的说法。正在准备配置windows请勿关闭计算机时间长了方法一…...

    2022/11/19 21:16:59
  43. 配置失败还原请勿关闭计算机,配置Windows Update失败,还原更改请勿关闭计算机...

    我们使用电脑的过程中有时会遇到这种情况&#xff0c;当我们打开电脑之后&#xff0c;发现一直停留在一个界面&#xff1a;“配置Windows Update失败&#xff0c;还原更改请勿关闭计算机”&#xff0c;等了许久还是无法进入系统。如果我们遇到此类问题应该如何解决呢&#xff0…...

    2022/11/19 21:16:58
  44. 如何在iPhone上关闭“请勿打扰”

    Apple’s “Do Not Disturb While Driving” is a potentially lifesaving iPhone feature, but it doesn’t always turn on automatically at the appropriate time. For example, you might be a passenger in a moving car, but your iPhone may think you’re the one dri…...

    2022/11/19 21:16:57