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

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