Во-первых, поддерживаются не только те процессоры, которые перечислены в списке, а вообще любые, которые поддерживает компилятор.
Во-вторых, можно выводить промежуточный код компилятора.
Всё это возможно благодаря тому, что мы можем сами задавать аргументы командной строки компилятора. Итак, выбираем из списка компилятор "x86-64 clang 3.9.0", и вводим в левое окно какую-нибудь функцию, например:
int foo(int x) { return !!x; }
Очевидно, что функция возвращает 0, если x == 0, и 1, если x != 0.
С оптимизацией -O1 получаем:
foo(int): # @foo(int) xor eax, eax test edi, edi setne al ret
Теперь попробуем получить код для arm. Вводим параметры: -O1 -target arm
Получаем:
foo(int): @ @foo(int) cmp r0, #0 movne r0, #1 bx lr
Можно для aarch64:
foo(int): // @foo(int) cmp w0, #0 // =0 cset w0, ne ret
Можно для msp430:
foo(int): push.w r4 mov.w r1, r4 cmp.w #0, r15 mov.w r2, r12 rra.w r12 mov.w #1, r15 bic.w r12, r15 pop.w r4 ret
И так далее. Можно получить промежуточный код на ассемблере llvm. Для этого вводим: -O1 -emit-llvm
Получаем длинный текст с кучей служебной информации, но на самом деле самая суть в этом:
target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128" target triple = "x86_64-unknown-linux-gnu" ; Function Attrs: nounwind readnone uwtable define i32 @foo(int)(i32) local_unnamed_addr #0 !dbg !6 { tail call void @llvm.dbg.value(metadata i32 %0, i64 0, metadata !11, metadata !12), !dbg !13 %2 = icmp ne i32 %0, 0, !dbg !14 %3 = zext i1 %2 to i32, !dbg !15 ret i32 %3, !dbg !16 }
Здесь: target datalayout - информация о таргете, т.е. о процессоре, разрядность представления величин и их выравнивание, а также о big/little endian. Подробно сейчас я это расписывать не буду, но там на самом деле всё просто.
И сам ассемблер (без дебажной информации):
%2 = icmp ne i32 %0, 0, %3 = zext i1 %2 to i32, ret i32 %3,
Здесь стоит пояснить, что ассемблер llvm не работает с физическими регистрами, он использует именованные виртуальные регистры, которых может быть неограниченно много. Здесь это %0, %2, %3. Они имеют типы, обозначенные как i1, i32 и т.п., что (очевидно) означает целое число соответствующей разрядности. Также следует добавить, что в llvm нет знаковых или беззнаковых типов, просто каждая команда интерпретирует по-своему (в большинстве случаев, как знаковые).
В первой строке команда %2 = icmp ne i32 %0 означает: если регистр %0 не равен 0, то записать в %2 единицу, иначе записать 0.
Во второй строке %3 = zext i1 %2 to i32 написано буквально следующее: дополнить нулями %2 до 32 разрядов и записать в %3.
В третьей строке: ret i32 %3 функция возвращает значение, записанное в %3.
Вот такая интересная игрушка. Также можно заметить, что хотя промежуточный код llvm максимально абстрагируется от конкретного процессора, он всё же не является совсем аппаратно-независимым, т.к. нуждается в указании разрядности регистров. Для примера, скомпилируем то же самое для msp430 (16-разрядное ядро): -O1 -target msp430 -emit-llvm:
target datalayout = "e-m:e-p:16:16-i32:16:32-a:16-n8:16" target triple = "msp430" ; Function Attrs: nounwind readnone define i16 @foo(int)(i16) local_unnamed_addr #0 !dbg !6 { tail call void @llvm.dbg.value(metadata i16 %0, i64 0, metadata !11, metadata !12), !dbg !13 %2 = icmp ne i16 %0, 0, !dbg !14 %3 = zext i1 %2 to i16, !dbg !15 ret i16 %3, !dbg !16 }
Суть та же, но регистры стали 16-разрядными. Для более сложных программ это может привести к существенным изменениям в коде.
В общем, интересная игрушка.