Какое разумное минимальное количество инструкций по сборке для небольшой C программы, включая установку? - PullRequest
4 голосов
/ 06 марта 2020

Я пытаюсь сгенерировать наименьшую возможную программу C, чтобы увидеть, сколько инструкций выполняется при ее запуске. Я отключил использование библиотек и отключил vdso. Тем не менее, моя C программа, которая, как говорит GDB, представляет собой 7 инструкций по сборке, в итоге выполняет 17k инструкций в соответствии с характеристиками perf.

Это нормальное количество инструкций только для настройки программы? Согласно GDB, код из ld- linux -x86-64.so.2 отображается в адресное пространство программы. Учитывая, что я отключил vdso и у меня нет библиотек, нужен ли этот файл для запуска программы? Может ли это быть причиной инструкций 17k?

Моя C программа foo5. c

int main(){
    char* str = "Hello World";
    return 0;
}

Как я компилирую:

gcc -nostdlib -nodefaultlibs stubstart.S -o foo5 foo5.c

stubstart.S

.globl _start
_start:call main;
    movl $1, %eax; 
    xorl %ebx, %ebx; 
    int $0x80

perf выход статистики:

Performance counter stats for './foo5':

              0.60 msec task-clock:u              #    0.015 CPUs utilized          
                 0      context-switches:u        #    0.000 K/sec                  
                 0      cpu-migrations:u          #    0.000 K/sec                  
                11      page-faults:u             #    0.018 M/sec                  
            46,646      cycles:u                  #    0.077 GHz                    
            17,224      instructions:u            #    0.37  insn per cycle         
             5,145      branches:u                #    8.513 M/sec                  
               435      branch-misses:u           #    8.45% of all branches  

gdb макет программы :

`/home/foo5', file type elf64-x86-64.
    Entry point: 0x5555555542b1
    0x0000555555554238 - 0x0000555555554254 is .interp
    0x0000555555554254 - 0x0000555555554278 is .note.gnu.build-id
    0x0000555555554278 - 0x0000555555554294 is .gnu.hash
    0x0000555555554298 - 0x00005555555542b0 is .dynsym
    0x00005555555542b0 - 0x00005555555542b1 is .dynstr
    0x00005555555542b1 - 0x00005555555542d5 is .text
    0x00005555555542d5 - 0x00005555555542e1 is .rodata
    0x00005555555542e4 - 0x00005555555542f8 is .eh_frame_hdr
    0x00005555555542f8 - 0x0000555555554330 is .eh_frame
    0x0000555555754f20 - 0x0000555555755000 is .dynamic
    0x00007ffff7dd51c8 - 0x00007ffff7dd51ec is .note.gnu.build-id in /lib64/ld-linux-x86-64.so.2
    0x00007ffff7dd51f0 - 0x00007ffff7dd52c4 is .hash in /lib64/ld-linux-x86-64.so.2
    0x00007ffff7dd52c8 - 0x00007ffff7dd53c0 is .gnu.hash in /lib64/ld-linux-x86-64.so.2
    0x00007ffff7dd53c0 - 0x00007ffff7dd56f0 is .dynsym in /lib64/ld-linux-x86-64.so.2
    0x00007ffff7dd56f0 - 0x00007ffff7dd5914 is .dynstr in /lib64/ld-linux-x86-64.so.2
    0x00007ffff7dd5914 - 0x00007ffff7dd5958 is .gnu.version in /lib64/ld-linux-x86-64.so.2
    0x00007ffff7dd5958 - 0x00007ffff7dd59fc is .gnu.version_d in /lib64/ld-linux-x86-64.so.2
    0x00007ffff7dd5a00 - 0x00007ffff7dd5dd8 is .rela.dyn in /lib64/ld-linux-x86-64.so.2
    0x00007ffff7dd5dd8 - 0x00007ffff7dd5e80 is .rela.plt in /lib64/ld-linux-x86-64.so.2
    0x00007ffff7dd5e80 - 0x00007ffff7dd5f00 is .plt in /lib64/ld-linux-x86-64.so.2
    0x00007ffff7dd5f00 - 0x00007ffff7dd5f08 is .plt.got in /lib64/ld-linux-x86-64.so.2
    0x00007ffff7dd5f10 - 0x00007ffff7df4b20 is .text in /lib64/ld-linux-x86-64.so.2
    0x00007ffff7df4b20 - 0x00007ffff7df9140 is .rodata in /lib64/ld-linux-x86-64.so.2
    0x00007ffff7df9140 - 0x00007ffff7df9141 is .stapsdt.base in /lib64/ld-linux-x86-64.so.2
    0x00007ffff7df9144 - 0x00007ffff7df97b0 is .eh_frame_hdr in /lib64/ld-linux-x86-64.so.2
    0x00007ffff7df97b0 - 0x00007ffff7dfbc24 is .eh_frame in /lib64/ld-linux-x86-64.so.2
    0x00007ffff7ffc680 - 0x00007ffff7ffce64 is .data.rel.ro in /lib64/ld-linux-x86-64.so.2
    0x00007ffff7ffce68 - 0x00007ffff7ffcfd8 is .dynamic in /lib64/ld-linux-x86-64.so.2
    0x00007ffff7ffcfd8 - 0x00007ffff7ffcfe8 is .got in /lib64/ld-linux-x86-64.so.2
    0x00007ffff7ffd000 - 0x00007ffff7ffd050 is .got.plt in /lib64/ld-linux-x86-64.so.2
    0x00007ffff7ffd060 - 0x00007ffff7ffdfd8 is .data in /lib64/ld-linux-x86-64.so.2
    0x00007ffff7ffdfe0 - 0x00007ffff7ffe170 is .bss in /lib64/ld-linux-x86-64.so.2

ОБНОВЛЕНИЕ:

В конце, комментарий шута о создании стандартного исполняемого файла вместо P IE для удаления ld.so путем добавления -no-p ie flag to g cc уменьшил показатель команды perf до 12. Затем предложение old_timer -O2 еще больше уменьшило его до 7! Спасибо всем.

ОБНОВЛЕНИЕ 2: Выбранный ответ с использованием -stati c также уменьшает количество команд с 17k до 12. Отличный ответ.

Также эта статья , связанная комментариями, актуальна и интересна.

Ответы [ 3 ]

4 голосов
/ 06 марта 2020

TL: DR: -static не по умолчанию, используйте его для создания исполняемого файла ELF, который запускает только ваш _start.

-no-pie -nostdlib также сделает c исполняемый файл просто потому, что он не является P IE и нет никаких динамических c библиотек для связи.

Также существует такая вещь, как -static-pie, когда ядро ​​загружает ваш исполняемый файл в случайный базовый адрес, но не сначала запустите ld.so (я думаю), но это не то, что вы получаете с -static.


Просто чтобы прояснить, мы Вы говорите о Dynami c Количество команд (сколько фактически выполнено в пользовательском пространстве, perf stat -e instructions:u), а не подсчет c ( сколько находится на диске / в памяти как часть исполняемого файла). Stati c count подсчитывает только инструкции внутри циклов и по-прежнему считает команды, которые никогда не выполняются.

Или, по крайней мере, я отвечаю. Это делает метаданные в других разделах и код, который не выполняется, не имеет значения.

Согласно GDB, код из ld- linux -x86-64.so.2 отображается в адрес программы пространство. Учитывая, что я отключил vdso и не включаю библиотеки, нужен ли этот файл для запуска программы?

Вы все еще создали независимый от позиции исполняемый файл (P IE). Это общий объект ELF с точкой входа, поэтому он все еще динамически связан. Так что на нем работает интерпретатор ld.so ELF. Ничего не поделаешь, потому что вы на самом деле не используете какие-либо общие библиотеки, но инструкции из 17k пространства пользователя звучат правильно. Я получаю 32606 или 7 инструкций для вашей программы в моей системе Arch Linux (glib c 2.31).

ld.so запускается как "интерпретатор" для вашего двоичного файла аналогично тому, как /bin/sh запускается для интерпретации исполняемого текстового файла, начинающегося с #!/bin/sh. (Хотя загрузчик программ ELF Linux все еще выполняет некоторую работу по отображению сегментов программы в память в соответствии с заголовком программы исполняемого файла, поэтому ld.so не должен делать это вручную с помощью системных вызовов.)

Вы можете увидеть это, запустив под gdb ./foo5 и используя starti вместо run для остановки перед первой инструкцией пространства пользователя. Вы увидите, что вы находитесь в ld.so _start.

Reading symbols from ./foo5...
(No debugging symbols found in ./foo5)
Cannot access memory at address 0x1024   ### note this isn't a real address,
                     ### just an offset relative to the base address / start of the file.
                     ### That's another clue this is a PIE
(gdb) starti

Program stopped.
0x00007ffff7fd3100 in _start () from /lib64/ld-linux-x86-64.so.2

. Вы также можете запустить strace ./foo5, чтобы увидеть, какие системные вызовы он выполняет, что свидетельствует о наличии множества что-то происходит:

$ strace ./foo5
execve("./foo5", ["./foo5"], 0x7ffc12394d90 /* 50 vars */) = 0
brk(NULL)                               = 0x55741b4b7000
arch_prctl(0x3001 /* ARCH_??? */, 0x7ffca69312b0) = -1 EINVAL (Invalid argument)
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f1d4fc4b000
arch_prctl(ARCH_SET_FS, 0x7f1d4fc4ba80) = 0
mprotect(0x557419622000, 4096, PROT_READ) = 0
strace: [ Process PID=303809 runs in 32 bit mode. ]
exit(0)                                 = ?

(Обратите внимание на «работает в 32-битном режиме»; это не так, но strace обнаружил, что вы использовали 32-битный int $0x80 ABI вместо обычного syscall ABI который использовался ld.so.)


Использовать -static

-nostdlib, используемый для обозначения -static, в G CC, сконфигурированном, чтобы не делать PIEs по умолчанию. Но современные дистрибутивы действительно настраивают G CC, чтобы сделать пироги из соображений безопасности. См. 32-разрядные абсолютные адреса, больше не разрешенные в x86-64 Linux?

$ file foo5
foo5: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=1ac0a9af247fefebde100695805e5b73f06e891c, not stripped

После сборки с -static, OTOH:

$ file foo5
foo5: ELF 64-bit LSB executable ...
$ perf stat --all-user ./foo5

 Performance counter stats for './foo5':

              0.03 msec task-clock                #    0.151 CPUs utilized          
                 0      context-switches          #    0.000 K/sec                  
                 0      cpu-migrations            #    0.000 K/sec                  
                 1      page-faults               #    0.030 M/sec                  
             1,930      cycles                    #    0.058 GHz                    
                12      instructions              #    0.01  insn per cycle         
                 4      branches                  #    0.121 M/sec                  
                 0      branch-misses             #    0.00% of all branches        

       0.000219151 seconds time elapsed

       0.000284000 seconds user
       0.000000000 seconds sys

(Странно, что perf не печатает :u для событий, когда вы используете --all-user. В моей системе /proc/sys/kernel/perf_event_paranoid = 0, поэтому, если я не использую это, он также считает команды, выполняемые внутри ядра. Это значительно варьируется от запуска к запуску, но всего около 60 тыс. для этого исполняемого файла stati c.

Я считаю только 11 выполняемых инструкций пространства пользователя, но, очевидно, мой i7-6700k насчитывает 12 для этого события. (Существует аппаратная поддержка для маскировки пользователя, ядра или обоих для любого счетчика событий. Это то, что использует perf.)

GDB также подтверждает успех:

Reading symbols from ./foo5...
(No debugging symbols found in ./foo5)
Cannot access memory at address 0x401024
(gdb) starti
Starting program: /tmp/foo5

Program stopped.
0x0000000000401000 in _start ()
(gdb) 

И окно разборки из layout reg показывает:

│  >0x401000 <_start>       call   0x40100e <main>
│   0x401005 <_start+5>     mov    eax,0x1
│   0x40100a <_start+10>    xor    ebx,ebx
│   0x40100c <_start+12>    int    0x80
│   0x40100e <main>         push   rbp
│   0x40100f <main+1>       mov    rbp,rsp
│   0x401012 <main+4>       lea    rax,[rip+0xfe7]        # 0x402000
│   0x401019 <main+11>      mov    QWORD PTR [rbp-0x8],rax
│   0x40101d <main+15>      mov    eax,0x0
│   0x401022 <main+20>      pop    rbp
│   0x401023 <main+21>      ret

Вы могли бы скомпилировать с -O2, чтобы оптимизировать main до xor eax,eax / ret, или вообще не вызывать его, поэтому только 3 пользователя инструкции должны быть выполнены.

Или для оптимизации подсчета команд в пользовательском пространстве при использовании C, см. @ mosvy's answer о написании _start в C и встроенный asm _exit(2), который может встраиваться в него.)

Обратите внимание, что вашему _start не удается передать arg c и argv в main, хотя RSP правильно выровнен на 16 байт перед вызовом функции. (Поскольку x86-64 SysV ABI гарантирует, что вход в процесс происходит с выровненным стеком). Вы можете сделать это с помощью MOV Load и LEA. Обратите внимание, что, поскольку вы не инициализируете lib c, даже если вы статически связали lib c, вы не можете вызывать ее функции.

См. Как получить значение аргументов с помощью встроенной сборки в C без Glib c? для некоторых хаков. (В основном автономный asm _start, написанный в операторе asm() в глобальном масштабе, или мой ответ - полный взлом соглашения о вызовах.)

2 голосов
/ 06 марта 2020

Версия x86-64:

foo. c

#include <sys/syscall.h>
static void __attribute__((noreturn)) _exit(int s){
        __asm__ __volatile__ ( "syscall" :: "D"(s), "A"(SYS_exit) );
        __builtin_unreachable();
}
void _start(void){ _exit(0); }
$ cc -nostdlib -static -nostartfiles -Os foo.c -o foo
$ ./foo
$ echo $?
0
$ objdump -d ./foo
./foo:     file format elf64-x86-64


Disassembly of section .text:

0000000000401000 <_start>:
  401000:       31 ff                   xor    %edi,%edi
  401002:       b8 3c 00 00 00          mov    $0x3c,%eax
  401007:       0f 05                   syscall

Компилятор также сгенерирует некоторые дополнительные разделы, которые не нужны в этом небольшом примере программы. Вы не должны беспокоиться о них, но если хотите, вы можете удалить некоторые из них, скомпилировав:

$ cc -nostdlib -static -nostartfiles -Wl,--build-id=none -fno-asynchronous-unwind-tables -Os foo.c -o foo
$ objcopy -R .comment foo

-Wl,--build-id=none избавится от .note.gnu.build-id и -fno-asynchronous-unwind-tables избавится .eh_frame. .comment может быть удален вручную, он не является частью какого-либо сопоставленного сегмента.

0 голосов
/ 06 марта 2020
.globl _start
_start:
    call main
    movl $1, %eax
    xorl %ebx, %ebx
    int $0x80


int main(){
    return 0;
}

gcc -O2 -nostdlib -nodefaultlibs stubstart.S foo5.c -o foo5
objdump -D foo5

00000000000002c0 <main>:
 2c0:   31 c0                   xor    %eax,%eax
 2c2:   c3                      retq   

00000000000002c3 <_start>:
 2c3:   e8 f8 ff ff ff          callq  2c0 <main>
 2c8:   b8 01 00 00 00          mov    $0x1,%eax
 2cd:   31 db                   xor    %ebx,%ebx
 2cf:   cd 80                   int    $0x80

6 инструкций. 17 байтов. строка была мертвым кодом.

меньше

.globl _start
_start:
    movl $1, %eax
    xorl %ebx, %ebx
    int $0x80

gcc -O2 -nostdlib -nodefaultlibs stubstart.S -o foo5
objdump -D foo5

0000000000000241 <_start>:
 241:   b8 01 00 00 00          mov    $0x1,%eax
 246:   31 db                   xor    %ebx,%ebx
 248:   cd 80                   int    $0x80

три инструкции

...