elf 文件格式探秘——程序运行背后的故事

摘要:本文主要讲解elf文件格式,通过readelf命令结合底层的相关数据结构,讲解相关内容,分析程序运行的基本原理。

本文来源:elf 文件格式探秘——程序运行背后的故事 http://blog.csdn.net/trochiluses/article/details/10373921

1.elf 文件格式概览



elf文件大体上由文件头和相关的section组成,而每个section由header和data组成。


2.elf文件头


文件头的数据结构:

 1 
                  typedef struct {
            unsigned char e_ident[EI_NIDENT];
            Elf32_Half    e_type;
  5         Elf32_Half    e_machine;
            Elf32_Word    e_version;
            Elf32_Addr    e_entry;
            Elf32_Off     e_phoff;
            Elf32_Off     e_shoff;
 10         Elf32_Word    e_flags;
            Elf32_Half    e_ehsize;
            Elf32_Half    e_phentsize;
            Elf32_Half    e_phnum;
            Elf32_Half    e_shentsize;
 15         Elf32_Half    e_shnum;
            Elf32_Half    e_shstrndx;
    } Elf32_Ehdr;


使用readelf命令查看文件头:

$ readelf -h /bin/ls

  1 ELF Header:
  2   Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  3   Class:                             ELF32
  4   Data:                              2's complement, little endian
  5   Version:                           1 (current)
  6   OS/ABI:                            UNIX - System V
  7   ABI Version:                       0
  8   Type:                              EXEC (Executable file)
  9   Machine:                           Intel 80386
 10   Version:                           0x1
 11   Entry point address:               0x804be34
 12   Start of program headers:          52 (bytes into file)
 13   Start of section headers:          103388 (bytes into file)
 14   Flags:                             0x0
 15   Size of this header:               52 (bytes)
 16   Size of program headers:           32 (bytes)
 17   Number of program headers:         9
 18   Size of section headers:           40 (bytes)
 19   Number of section headers:         28
 20   Section header string table index: 27

在 readelf 的输出中
第 1 行,ELF Header: 指名 ELF 文件头开始。
第 2 行,Magic 魔数,用来指名该文件是一个 ELF 目标文件。第一个字节 7F 是个固定的数;后面的 3 个字节正是 E, L, F 三个字母的 ASCII 形式。
第 3 行,CLASS 表示文件类型,这里是 32位的 ELF 格式。
第 4 行,Data 表示文件中的数据是按照什么格式组织(大端或小端)的,不同处理器平台数据组织格式可能就不同,如x86平台为小端存储格式。
第 5 行,当前 ELF 文件头版本号,这里版本号为 1 。
第 6 行,OS/ABI ,指出操作系统类型,ABI 是 Application Binary Interface 的缩写。
第 7 行,ABI 版本号,当前为 0 。
第 8 行,Type 表示文件类型。ELF 文件有 3 种类型,一种是如上所示的 Relocatable file 可重定位目标文件,一种是可执行文件(Executable),另外一种是共享库(Shared Library) 。
第 9 行,机器平台类型。
第 10 行,当前目标文件的版本号。
第 11 行,程序的虚拟地址入口点,因为这还不是可运行的程序,故而这里为零。
第 12 行,与 11 行同理,这个目标文件没有 Program Headers。
第 13 行,sections 头开始处,这里 208 是十进制,表示从地址偏移 0xD0 处开始。
第 14 行,是一个与处理器相关联的标志,x86 平台上该处为 0 。
第 15 行,ELF 文件头的字节数。
第 16 行,程序头的大小。
第 17 行,segment的个数9。
第 18 行,sections header 的大小,这里每个 section 头大小为 40 个字节。
第 19 行,一共有多少个 section 头,这里是 8 个。
第 20 行,section 头字符串表索引号,从 Section Headers 输出部分可以看到其内容的偏移在 0xa0 处,从此处开始到0xcf 结束保存着各个 sections 的名字,如 .data,.text,.bss等。


e_ident是elf文件的第一项,以一个magic字节开头。第一个字节是0x7f,然后紧跟“ELF”,我们可以用hexdump来查看它。

$ hexdump -C /bin/ls | more
    00000000  7f 45 4c 46 01 02 01 00  00 00 00 00 00 00 00 00  |.ELF............|
    
  5 ... (rest of the program follows) ...

关于程序入口——不是main的地址:

  1 
                  $ cat test.c
    #include <stdio.h>
    
  5 int main(void)
    {
            printf("main is : %p
", &main);
            return 0;
    }
 10 
    $ gcc -Wall -o test test.c
    
    $ ./test
    main is : 0x10000430
 15 
    $ readelf --headers ./test | grep 'Entry point'
      Entry point address:               0x100002b0
    
    $ objdump --disassemble ./test | grep 100002b0
 20 100002b0 <_start>:
    100002b0:       7c 29 0b 78     mr      r9,r1
    

实际上,程序入口地址是“_start”,而这个地址空间和程序运行时候的地址空间是独立的。 


在 Section Headers 这里,可以看到 .bss 和 .shstrtab 的偏移都为 0xa0 。这是因为,没有被初始化的全局变量,会在加载阶段被用 0 来初始化,这时候它和 .data 段一样可读可写。但在编译阶段,.data 段会被分配一部分空间已存放数据(这里从偏移 0x6c 开始),而 .bss 则没有,.bss 仅有的是 section headers 。
         链接器从 .rel.text  就可以知道哪些地方需要进行重定位(relocate) 。
        .symtab 是符号表。
         Ndx 是符号表所在的 section 的 section header 编号。如 .data 段的 section header 编号是 3,而string1,string2,lenght 都是在 .data 段的。


3.符号表和重定位


   符号表是符号和地址的映射,主要用于链接:如果有一个外部申明extern in function,我们引用了function,那么连接器就要查找function对应的地址,从而来调用它。

    和符号表紧密相关的是重定位,我们可以在“动态链接”这一节详细理解什么是重定位。


4.section and segments


     elf文件格式制定了elf文件的两种视角:链接和执行。这样给系统的设计者提供了很大的灵活性。

    我们接下来讲解目标文件中的section,这些section将被链接成可执行文件。一个或者多个section被映射到一个segment里,segment属于可执行文的一部分。


4.1segments

    如同我们在前面提到的那样,elf文件除了包含整体的文件头以外,还包含各个部分的program headers,这些program headers向操作系统描述了程序load和execute所需要的一切信息。联系内存的段式管理,同一个段的属性是相同的,下面是一个传统的program headers的数据结构:

 1 
                  typedef struct {
              Elf32_Word p_type;
              Elf32_Off  p_offset;
  5           Elf32_Addr p_vaddr;
              Elf32_Addr p_paddr;
              Elf32_Word p_filesz;
              Elf32_Word p_memsz;
              Elf32_Word p_flags;
 10           Elf32_Word p_align;
    }

        在elf总体的header里面,e_phoffe_phnum and e_phentsize表示program headers的偏移,数量和大小,有了这些信息我们可以轻易知道program headers的相关情况,从而利用它们。

        如同上面提到的那样,program headers并不仅仅是segments信息。p_type定义了program header的类型(PT_INTERP表明这个program header是一个string类型的指针,指向针对一个二进制文件的解释器)。我们原来对比过编译型语言和解释型语言,而且作出了以下区分:编译器建立了一个可以独立运行的二进制文件。那么为什么它需要一个解释器呢?一般而言,实际情况下更为复杂:现代系统装载可执行文件需要很大的灵活性,为了达到这一目的,一些信息只能在程序运行的时候获取。我们将在动态链接一章里解释这个问题。因此,需要对二进制文件做出一些小的更改以让它能够在实时运转的时候正常工作。因此,实际情况下的二进制解释器是一个动态装载器,它用一些列步骤完成可执行文件的加载和运行。

其他参数的解释:p_offset相对首地址的偏移,p_vaddr虚拟地址(一些嵌入式系统中没有实现虚拟内存,那么它等于p_paddr),p_paddr物理内存地址;p_filesz文件大小;p_memsz实际在内存中占的空间大小,一般大于前者,多余部分用0填充;p_flags读写可执行权限位;p_align对齐.


看例子:

$ readelf -l /bin/ls


Elf file type is EXEC (Executable file)
Entry point 0x804be34
There are 9 program headers, starting at offset 52


Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  PHDR           0x000034 0x08048034 0x08048034 0x00120 0x00120 R E 0x4
  INTERP         0x000154 0x08048154 0x08048154 0x00013 0x00013 R   0x1
      [Requesting program interpreter: /lib/ld-linux.so.2]
  LOAD           0x000000 0x08048000 0x08048000 0x18d24 0x18d24 R E 0x1000
  LOAD           0x018ef8 0x08061ef8 0x08061ef8 0x003e8 0x01008 RW  0x1000
  DYNAMIC        0x018f0c 0x08061f0c 0x08061f0c 0x000e0 0x000e0 RW  0x4
  NOTE           0x000168 0x08048168 0x08048168 0x00044 0x00044 R   0x4
  GNU_EH_FRAME   0x015f7c 0x0805df7c 0x0805df7c 0x006f4 0x006f4 R   0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x4
  GNU_RELRO      0x018ef8 0x08061ef8 0x08061ef8 0x00108 0x00108 R   0x1


 Section to Segment mapping:
  Segment Sections...
   00     
   01     .interp 
   02     .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame 
   03     .ctors .dtors .jcr .dynamic .got .got.plt .data .bss 
   04     .dynamic 
   05     .note.ABI-tag .note.gnu.build-id 
   06     .eh_frame_hdr 
   07     
   08     .ctors .dtors .jcr .dynamic .got 

上述各段组成了最终在内存中执行的程序,其还提供了各段在虚拟地址空间和物理地址空间中的大小、位置、标志、访问授权和对齐方面的信息。各段语义如下:

PHDR保存程序头表

INTERP指定程序从可行性文件映射到内存之后,必须调用的解释器,它是通过链接其他库来满足未解析的引用,用于在虚拟地址空间中插入程序运行所需的动态库。

LOAD表示一个需要从二进制文件映射到虚拟地址空间的段,其中保存了常量数据(如字符串),程序目标代码等。

DYNAMIC段保存了由动态连接器(即INTERP段中指定的解释器)使用的信息。


4.2sections

   多个section组成segment。section将二进制文件组织成逻辑区域,用于在编译器和链接器之间交流信息。section的头部和segment有些类似:

  1 
                  typedef struct {
              Elf32_Word sh_name;
              Elf32_Word sh_type;
  5           Elf32_Word sh_flags;
              Elf32_Addr sh_addr;
              Elf32_Off  sh_offset;
              Elf32_Word sh_size;
              Elf32_Word sh_link;
 10           Elf32_Word sh_info;
              Elf32_Word sh_addralign;
              Elf32_Word sh_entsize;
    }
    
 15             

     sh_type具有更多的类型:SH_PROGBITS表明这是程序将要使用的数据的seciton,其他的值表示是否它是一个符号表等等。allocate属性表示这个section需要被分配内存。接下来,看一个实例:

  1 
                  #include <stdio.h>
    
    int big_big_array[10*1024*1024];
  5 
    char *a_string = "Hello, World!";
    
    int a_var_with_value = 0x100;
    
 10 int main(void)
    {
    	big_big_array[0] = 100;
    	printf("%s
", a_string);
    	a_var_with_value += 20;
 15 }
    

编译后执行:

  1 
                  $ readelf --all ./sections
    ELF Header:
     ...
  5   Size of section headers:           40 (bytes)
      Number of section headers:         37
      Section header string table index: 34
    
    Section Headers:
 10   [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
      [ 0]                   NULL            00000000 000000 000000 00      0   0  0
      [ 1] .interp           PROGBITS        10000114 000114 00000d 00   A  0   0  1
      [ 2] .note.ABI-tag     NOTE            10000124 000124 000020 00   A  0   0  4
      [ 3] .hash             HASH            10000144 000144 00002c 04   A  4   0  4
 15   [ 4] .dynsym           DYNSYM          10000170 000170 000060 10   A  5   1  4
      [ 5] .dynstr           STRTAB          100001d0 0001d0 00005e 00   A  0   0  1
      [ 6] .gnu.version      VERSYM          1000022e 00022e 00000c 02   A  4   0  2
      [ 7] .gnu.version_r    VERNEED         1000023c 00023c 000020 00   A  5   1  4
      [ 8] .rela.dyn         RELA            1000025c 00025c 00000c 0c   A  4   0  4
 20   [ 9] .rela.plt         RELA            10000268 000268 000018 0c   A  4  25  4
      [10] .init             PROGBITS        10000280 000280 000028 00  AX  0   0  4
      [11] .text             PROGBITS        100002b0 0002b0 000560 00  AX  0   0 16
      [12] .fini             PROGBITS        10000810 000810 000020 00  AX  0   0  4
      [13] .rodata           PROGBITS        10000830 000830 000024 00   A  0   0  4
 25   [14] .sdata2           PROGBITS        10000854 000854 000000 00   A  0   0  4
      [15] .eh_frame         PROGBITS        10000854 000854 000004 00   A  0   0  4
      [16] .ctors            PROGBITS        10010858 000858 000008 00  WA  0   0  4
      [17] .dtors            PROGBITS        10010860 000860 000008 00  WA  0   0  4
      [18] .jcr              PROGBITS        10010868 000868 000004 00  WA  0   0  4
 30   [19] .got2             PROGBITS        1001086c 00086c 000010 00  WA  0   0  1
      [20] .dynamic          DYNAMIC         1001087c 00087c 0000c8 08  WA  5   0  4
      [21] .data             PROGBITS        10010944 000944 000008 00  WA  0   0  4
      [22] .got              PROGBITS        1001094c 00094c 000014 04 WAX  0   0  4
      [23] .sdata            PROGBITS        10010960 000960 000008 00  WA  0   0  4
 35   [24] .sbss             NOBITS          10010968 000968 000000 00  WA  0   0  1
      [25] .plt              NOBITS          10010968 000968 000060 00 WAX  0   0  4
      [26] .bss              NOBITS          100109c8 000968 2800004 00  WA  0   0  4
      [27] .comment          PROGBITS        00000000 000968 00018f 00      0   0  1
      [28] .debug_aranges    PROGBITS        00000000 000af8 000078 00      0   0  8
 40   [29] .debug_pubnames   PROGBITS        00000000 000b70 000025 00      0   0  1
      [30] .debug_info       PROGBITS        00000000 000b95 0002e5 00      0   0  1
      [31] .debug_abbrev     PROGBITS        00000000 000e7a 000076 00      0   0  1
      [32] .debug_line       PROGBITS        00000000 000ef0 0001de 00      0   0  1
      [33] .debug_str        PROGBITS        00000000 0010ce 0000f0 01  MS  0   0  1
 45   [34] .shstrtab         STRTAB          00000000 0011be 00013b 00      0   0  1
      [35] .symtab           SYMTAB          00000000 0018c4 000c90 10     36  65  4
      [36] .strtab           STRTAB          00000000 002554 000909 00      0   0  1
    Key to Flags:
      W (write), A (alloc), X (execute), M (merge), S (strings)
 50   I (info), L (link order), G (group), x (unknown)
      O (extra OS processing required) o (OS specific), p (processor specific)
    
    There are no section groups in this file.
     ...
 55 
    Symbol table '.symtab' contains 201 entries:
       Num:    Value  Size Type    Bind   Vis      Ndx Name
    ...
        99: 100109cc 0x2800000 OBJECT  GLOBAL DEFAULT   26 big_big_array
 60 ...
       110: 10010960     4 OBJECT  GLOBAL DEFAULT   23 a_string
    ...
       130: 10010964     4 OBJECT  GLOBAL DEFAULT   23 a_var_with_value
    ...
 65    144: 10000430    96 FUNC    GLOBAL DEFAULT   11 main
    
                

     上面的输出中,我们为了方面分析,截断了一些冗余信息。

     首先,来看big_big_array变量:在0x100109cc处,我们可以看见它在.bss这个段中(因为0x100109cc是.bss开始的地方),注意.bss的属性nobits,表明它在磁盘上是不占用数据存储空间的。同样,我们可以分析其他变量和main函数,注意main函数在.text段中。

     或者使用如下命令:

$ readelf -S /bin/ls
There are 28 section headers, starting at offset 0x193dc:


Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .interp           PROGBITS        08048154 000154 000013 00   A  0   0  1
  [ 2] .note.ABI-tag     NOTE            08048168 000168 000020 00   A  0   0  4
  [ 3] .note.gnu.build-i NOTE            08048188 000188 000024 00   A  0   0  4
  [ 4] .gnu.hash         GNU_HASH        080481ac 0001ac 00006c 04   A  5   0  4
  [ 5] .dynsym           DYNSYM          08048218 000218 0007a0 10   A  6   1  4
  [ 6] .dynstr           STRTAB          080489b8 0009b8 00058c 00   A  0   0  1
  [ 7] .gnu.version      VERSYM          08048f44 000f44 0000f4 02   A  5   0  2
  [ 8] .gnu.version_r    VERNEED         08049038 001038 0000e0 00   A  6   3  4
  [ 9] .rel.dyn          REL             08049118 001118 000038 08   A  5   0  4
  [10] .rel.plt          REL             08049150 001150 000350 08   A  5  12  4
  [11] .init             PROGBITS        080494a0 0014a0 00002e 00  AX  0   0  4
  [12] .plt              PROGBITS        080494d0 0014d0 0006b0 04  AX  0   0 16
  [13] .text             PROGBITS        08049b80 001b80 0104ac 00  AX  0   0 16
  [14] .fini             PROGBITS        0805a02c 01202c 00001a 00  AX  0   0  4
  [15] .rodata           PROGBITS        0805a060 012060 003f1b 00   A  0   0 32
  [16] .eh_frame_hdr     PROGBITS        0805df7c 015f7c 0006f4 00   A  0   0  4
  [17] .eh_frame         PROGBITS        0805e670 016670 0026b4 00   A  0   0  4
  [18] .ctors            PROGBITS        08061ef8 018ef8 000008 00  WA  0   0  4
  [19] .dtors            PROGBITS        08061f00 018f00 000008 00  WA  0   0  4
  [20] .jcr              PROGBITS        08061f08 018f08 000004 00  WA  0   0  4
  [21] .dynamic          DYNAMIC         08061f0c 018f0c 0000e0 08  WA  6   0  4
  [22] .got              PROGBITS        08061fec 018fec 000008 04  WA  0   0  4
  [23] .got.plt          PROGBITS        08061ff4 018ff4 0001b4 04  WA  0   0  4
  [24] .data             PROGBITS        080621c0 0191c0 000120 00  WA  0   0 32
  [25] .bss              NOBITS          080622e0 0192e0 000c20 00  WA  0   0 32
  [26] .gnu_debuglink    PROGBITS        00000000 0192e0 000008 00      0   0  1
  [27] .shstrtab         STRTAB          00000000 0192e8 0000f2 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

PROGBITS(程序必须解释的信息,如二进制代码),STRTAB用于存储与ELF格式有关的字符串,但与程序没有直接关联,如各个节的名称(.text, .comment)

.data保存初始化过的数据,这是普通程序数据的一部分,可以在程序运行期间修改。

.rodata保存了只读数据,可以读取但不能修改,例如printf语句中的所有静态字符串封装到该节。

.init.fini保存了进程初始化和结束所用的代码,这通常是由编译器自动添加的。

.hash是一个散列表,允许在不对全表元素进行线性搜索的情况下,快速访问所有符号表项。


4.3section&&segment

  1 
                  $ readelf --segments /bin/ls
    
    Elf file type is EXEC (Executable file)
  5 Entry point 0x100026c0
    There are 8 program headers, starting at offset 52
    
    Program Headers:
      Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
 10   PHDR           0x000034 0x10000034 0x10000034 0x00100 0x00100 R E 0x4
      INTERP         0x000154 0x10000154 0x10000154 0x0000d 0x0000d R   0x1
          [Requesting program interpreter: /lib/ld.so.1]
      LOAD           0x000000 0x10000000 0x10000000 0x14d5c 0x14d5c R E 0x10000
      LOAD           0x014d60 0x10024d60 0x10024d60 0x002b0 0x00b7c RWE 0x10000
 15   DYNAMIC        0x014f00 0x10024f00 0x10024f00 0x000d8 0x000d8 RW  0x4
      NOTE           0x000164 0x10000164 0x10000164 0x00020 0x00020 R   0x4
      GNU_EH_FRAME   0x014d30 0x10014d30 0x10014d30 0x0002c 0x0002c R   0x4
      GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RWE 0x4
    
 20  Section to Segment mapping:
      Segment Sections...
       00
       01     .interp
       02     .interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version .gnu.version_ r .rela.dyn .rela.plt .init .text .fini .rodata .eh_frame_hdr
 25    03     .data .eh_frame .got2 .dynamic .ctors .dtors .jcr .got .sdata .sbss .p lt .bss
       04     .dynamic
       05     .note.ABI-tag
       06     .eh_frame_hdr
       07
 30 
                

.interp section is placed into an INTERP flagged segment. Notice that readelf tells us it is requesting the interpreter /lib/ld.so.1; this is the dynamic linker which is run to prepare the binary for execution.

注意两个LOAD段在权限位上的区别。


5.Debugging

Tradionally the primary method of post mortem debugging is referred to as the core dump. The termcore comes from the original physical characteristics of magnetic core memory, which uses the orientation of small magnetic rings to store state.


  1 
                  $ cat coredump.c
    int main(void) {
    	char *foo = (char*)0x12345;
  5 	*foo = 'a';
    
    	return 0;
    }
    
 10 $ gcc -Wall -g -o coredump coredump.c
    
    $ ./coredump
    Segmentation fault (core dumped)
    
 15 $ file ./core
    ./core: ELF 32-bit LSB core file Intel 80386, version 1 (SYSV), SVR4-style, from './coredump'
    
    $ gdb ./coredump
    ...
 20 (gdb) core core
    [New LWP 31614]
    Core was generated by `./coredump'.
    Program terminated with signal 11, Segmentation fault.
    #0  0x080483c4 in main () at coredump.c:3
 25 3		*foo = 'a';
    (gdb)

5.1Symbols and Debugging Information

        gdb需要原始的可执行文件和core dump来提供debug session。我们可以使用gcc的-g选项来增加debug信息,这些debug信息将存储在elf文件的特殊section之中。这些debug信息包括变量大小,数组长度,寄存器使用等信息。

        尽管这些调试信息不是程序运行所必须的,但是它们占用了很大的磁盘空间。 objcopy工具可以用来提取这个debug信息(--only-keep-debug),接着可以给这些被截断的信息增加一个链接(--add-gnu-debuglink)。这样以后,原始的可执行文件中将多出一个称为 .gnu_debuglink的section,它包含一个hash,这样debugging session开启调试器能够确保它把正确的调试信息和可执行文件链接起来。

  1 
                  $ gcc -g -shared -o libtest.so libtest.c
    $ objcopy --only-keep-debug libtest.so libtest.debug
    $ objcopy --add-gnu-debuglink=libtest.debug libtest.so
  5 $ objdump -s -j .gnu_debuglink libtest.so
    
    libtest.so:     file format elf32-i386
    
    Contents of section .gnu_debuglink:
 10  0000 6c696274 6573742e 64656275 67000000  libtest.debug...
     0010 52a7fd0a                             R... 
    

6.readefl常用选项

  readelf [-a|--all]
               [-h|--file-header]
               [-l|--program-headers|--segments]
               [-S|--section-headers|--sections]
               [-s|--syms|--symbols]
                     
               elffile...

参考文献:

[1]Chapter 8. Behind the process coderwall.com

[2]ELF格式文件符号表全解析及readelf命令使用方法 http://blog.csdn.net/edonlii/article/details/8779075

原文地址:https://www.cnblogs.com/riskyer/p/3285733.html