пятница, 24 июня 2011 г.

Inside Go language

Появилось желание поковыряться в бинарниках, которые генерирует компилятор Go)
Хотелось посмотреть своими глазами, чем он отличается и увидеть, как реализован garbage collector (последнее пока не удалось).
Тактика была следующей - я написал несколько самых простых приложений на Go и, дабы иметь возможность сравнить, на С. Затем стал копаться в них с помощью различных отладчиков и hex-editor'ов. В качестве инструментов я выбрал gdb, DDD (front-end для gdb) и EDB + GHex. EDB сразу пришлось отмести, т.к. он хоть и выглядел как всеми любимая OllyDbg, но был как-то дико неудобен в использовании)
Для начала продемонстрирую листинги:

package main

import "fmt"

func print(input string) {
fmt.Printf("%s\n",input)
}

func main(){
test:="aaaa"
print(test)
}

Все чрезвычайно просто) Теперь посмотрим, какой бинарник получился после компиляции (упомяну, что собирать надо с отладочной информацией - т.е. не забыть про ключ -g)


0x08048c8b <+0>: mov    %gs:0x0,%ecx
0x08048c92 <+7>: mov    -0x8(%ecx),%ecx
0x08048c95 <+10>: cmp    (%ecx),%esp
0x08048c97 <+12>: ja     0x8048ca2 <main.main+23>
0x08048c99 <+14>: xor    %edx,%edx
0x08048c9b <+16>: xor    %eax,%eax
0x08048c9d <+18>: call   0x804966a <runtime.morestack>


Это начало функции main. Как можно заметить, в начале происходит некоторая работа - выбирается аргумент функции, затем проверяется его расположение. Если он находится не на вершине стека, то, видимо, есть еще аргументы и для них вызывается функция увеличения стека morestack

0x08048ca2 <+23>: sub    $0x20,%esp
0x08048ca5 <+26>: lea    0x808239c,%esi
0x08048cab <+32>: lea    0x18(%esp),%edi
0x08048caf <+36>: cld    
0x08048cb0 <+37>: movsl  %ds:(%esi),%es:(%edi)
0x08048cb1 <+38>: movsl  %ds:(%esi),%es:(%edi)
0x08048cb2 <+39>: lea    0x18(%esp),%esi
0x08048cb6 <+43>: lea    (%esp),%edi
0x08048cb9 <+46>: cld    
0x08048cba <+47>: movsl  %ds:(%esi),%es:(%edi)
0x08048cbb <+48>: movsl  %ds:(%esi),%es:(%edi)
0x08048cbc <+49>: call   0x8048c00 <main.print>


Далее происходит следующее - из данных выбирается необходимая строка "aaaa" и передается в нашу функцию print. Стоит заметить, что в Gо каждый поток имеет собственный стек, место для которого выделяется в куче. В функции printf также сначала проверяется количество аргументов (при необходимости вызывается morestack), затем выделяется память для копирования переданной на вход строки

0x08048c1a <+26>:    sub    $0x54,%esp
0x08048c1d <+29>:    movl   $0x8,(%esp)
0x08048c24 <+36>:    call   0x804e245 <runtime.new>


И, собственно, вызов Printf из пакета fmt

0x08048c82 <+130>:   call   0x805b367 <fmt.Printf>

Теперь, для сравнения, аналогичный код на C:

#include <stdio.h>

void print(char *input)
{
printf("%s\n",input);
}

int main()
{
char test[]="aaaa";
print(test);
}

Исследуя внутренности бинарника, который реализует этот код, мы, конечно, заметим достаточно отличий.

   0x08048427 <+0>:     push   %ebp
   0x08048428 <+1>:     mov    %esp,%ebp
   0x0804842a <+3>:     and    $0xfffffff0,%esp
   0x0804842d <+6>:     sub    $0x20,%esp
   0x08048430 <+9>:     mov    %gs:0x14,%eax
   0x08048436 <+15>:    mov    %eax,0x1c(%esp)
   0x0804843a <+19>:    xor    %eax,%eax

//строка "aaaa" явным образом помещается в стек
   0x0804843c <+21>:    movl   $0x61616161,0x17(%esp)
   
//поместить символ окончания строки
   0x08048444 <+29>:    movb   $0x0,0x1b(%esp)
   
//помещаем полученную строку в eax и вызываем print
   0x08048449 <+34>:    lea    0x17(%esp),%eax
   0x0804844d <+38>:    mov    %eax,(%esp)
   0x08048450 <+41>:    call   0x8048414 <print>
   
//проверка целостности стека   
   0x08048455 <+46>:    mov    0x1c(%esp),%edx
   0x08048459 <+50>:    xor    %gs:0x14,%edx
   0x08048460 <+57>:    je     0x8048467 <main+64>
   0x08048462 <+59>:    call   0x8048338 <__stack_chk_fail@plt>
   0x08048467 <+64>:    leave  
   0x08048468 <+65>:    ret

Судя по всему, gcc немного оптимизировал код, поместив строку "aaaa" в код. К тому же обнаружился stack protector. Конечно, на таком простом примере трудно заметить много особенностей, поэтому в качестве эксперимента я решил посмотреть другой код, где появится работа с памятью:

package main

func main(){
test := make([]int, 100)

for i:=0; i<100; i++ {
 test[i]=i;
}
}


Вновь получим бинарник и увидим - в начале также проверяется расположение аргумента. А что дальше?

//вызов make - в данном случае это обертка для функции makeslice
0x08048c41 <+65>: call   0x8051e14
0x08048c46 <+70>: lea    0x14(%esp),%ebx
0x08048c4a <+74>: mov    %ebx,%esi
0x08048c4c <+76>: lea    0x3c(%esp),%edi
0x08048c50 <+80>: cld    
0x08048c51 <+81>: movsl  %ds:(%esi),%es:(%edi)
0x08048c52 <+82>: movsl  %ds:(%esi),%es:(%edi)
0x08048c53 <+83>: movsl  %ds:(%esi),%es:(%edi)

//начало цикла
0x08048c54 <+84>: xor    %eax,%eax
0x08048c56 <+86>: cmp    $0x64,%eax
0x08048c59 <+89>: jge    0x8048c78 <main.main+120>
0x08048c5b <+91>: lea    0x3c(%esp),%ebx
0x08048c5f <+95>: mov    %eax,%ebp

//проверка попадания счетчика в диапазон индексов слайса
0x08048c61 <+97>: cmp    0x4(%ebx),%eax
0x08048c64 <+100>: jb     0x8048c6b <main.main+107>

//если счетчик вне границ - паникуем)
0x08048c66 <+102>: call   0x8050a23 <runtime.panicindex>

0x08048c6b <+107>: mov    (%ebx),%ebx
0x08048c6d <+109>: lea    (%ebx,%ebp,4),%ebx
0x08048c70 <+112>: mov    %eax,(%ebx)
0x08048c72 <+114>: inc    %eax
0x08048c73 <+115>: cmp    $0x64,%eax
0x08048c76 <+118>: jl     0x8048c5b <main.main+91>
0x08048c78 <+120>: add    $0x48,%esp
0x08048c7b <+123>: ret    

Как мы видим, вызов функции make преобразовывается в вызов makeslice. К тому же, оказалось, что Go обрабатывает неожиданную ситуацию в виде выхода за пределы слайса. Но вот признаки работа сборщика мусора так и не найдены.

Выводы, конечно, по таким небольшим примерам делать сложно, но уже вырисовывается тот факт, что Go на низком уровне обеспечивает некоторые интересные мелочи, такие как работа со стеком (напоминает стек в .net), но в то же время С-шный компилятор, конечно, генерирует более оптимизированные бинарники.



0коммент.:

Отправить комментарий