Введение в MSIL - Часть 2: Использование локальных переменных — Архив WASM.RU

Все статьи

Введение в MSIL - Часть 2: Использование локальных переменных — Архив WASM.RU

В этой части я расскажу об использовании локальных переменных. Без них программы были бы не слишком интересны. Чтобы объяснить, как использовать переменные, давайте напишем простую программу для сложения чисел.

Внутри метода локальные переменные задаются с помощью директивы .locals.

.locals init (int32 first,
              int32 second,
              int32 result)

Это утверждение декларирует, что у данного метода есть три локальные переменные. В этом случае они все оказались одного типа - int32, что является синонимом типа System.Int32. init задаёт, что переменные должны быть инициализированы значением по умолчанию данного типа. Также возможно опускать имена переменных, и в этом случае вы сможете обращаться к ним по их индексу (начиная с нуля). Разумеется, использование имён существенно повышает читабельность.

Прежде чем мы продолжим, я хочу убедиться, что вы понимаете, как используется стек в MSIL. Когда вы хотите передать требуемые значения инструкции, их нужно поместить в стек. Чтобы прочитать эти значения, инструкция берёт их из стека. Похожим образом, при вызове метода вам нужно поместить в стек ссылку на объект (если нужно), а за ней - аргументы. В процессе вызова метода все аргументы вместе с ссылкой на объект будут взяты из стека. Для помещения значения в стек используется инструкция ldloc, используемую с переменной, содержащей значение. Для того, чтобы взять значение из стека, используется инструкция stloc, которой указывается переменная, куда сохраняется значение. Также держите в уме, что значения (с учётом их типа) сохраняются прямо в стек, а объекты - нет, так как CLI этого не позволяет. Вместо этого в стек помещаются ссылки на них. Это похоже на C++-объект, находящийся в "куче", с указателем на него в стеке. Никогда не забывайте о стеке, читая эту серию статей. Это поможет вам понять, почему значения постоянно помещаются и берутся из него.

Следующим шагом будет получение чисел от пользователя.

ldstr "First number: "
call void [mscorlib]System.Console::Write(string)
call string [mscorlib]System.Console::ReadLine()
call int32 [mscorlib]System.Int32::Parse(string)
stloc first

Как я упомянул в первой части, инструкция ldstr помещает строку в стек и, а инструкция call вызывает метод Write, беря свои аргументы из стека. Затем вызывается метод ReadLine, возвращающий строку, которая помещается в стек, и, поскольку она уже там, нам остаётся просто вызвать метод Int32::Parse, берущий строку из стека и помещающий в него соответствующий in32-эквиваент. Обратите внимание, что для простоты я опустил всё, что связано с обработкой ошибок. Затем инструкция stloc достаёт значение из стека и сохраняет его в локальную переменную 'first'. Получение следующего числа происходит примерно также, не считая, что значение сохраняется в переменной 'second'.

Теперь, прочитав два числа из стандартного потока ввода, настало время, чтобы их сложить. Для этого можно использовать инструкцию add.

ldloc first
ldloc second
add
stloc result

Она берёт два числа из стека и считает сумму. Чтобы поместить оба числа в стек, мы используем ldloc. После выполнения инструкции add полученный результат помещается в стек и программа сохраняет его в переменную 'result', используя stloc.

Последний шаг - это отобразить результат пользователю.

ldstr "{0} + {1} = {2}"

ldloc first
box int32

ldloc second
box int32

ldloc result
box int32

call void [mscorlib]System.Console::WriteLine(string, object, object, object)

Мы использовали перегруженный вариант WriteLine, принимающую форматирующую строку и три объекта. Каждый аргумент должен быть помещён в стек один за другим. Так как числа имеют тип int32, нам нужно сделать из них объекты, иначе получится несовпадение с сигнатурой метода.

Инструкция ldloc помещает каждый аргумент в стек. Затем для каждого аргумента int32 используется инструкция box. Она достаёт значение из стека, делает на его основе объект и помещает обратно в стек ссылку на него.

Вот полная программа.

.method static void main()
{
    .entrypoint
    .maxstack 4
    
    .locals init (int32 first,
                  int32 second,
                  int32 result)

    ldstr "First number: "
    call void [mscorlib]System.Console::Write(string)
    call string [mscorlib]System.Console::ReadLine()
    call int32 [mscorlib]System.Int32::Parse(string)
    stloc first
    
    ldstr "Second number: "
    call void [mscorlib]System.Console::Write(string)
    call string [mscorlib]System.Console::ReadLine()
    call int32 [mscorlib]System.Int32::Parse(string)
    stloc second

    ldloc first
    ldloc second
    add
    stloc result

    ldstr "{0} + {1} = {2}"
    
    ldloc first
    box int32
    
    ldloc second
    box int32
    
    ldloc result
    box int32
    
    call void [mscorlib]System.Console::WriteLine(string, object, object, object)    

    ret
}

Последнее в этом примере, на что вам стоит обратить своё внимание, это то, что в методе указано максимальное количество используемых стек-слотов равное 4, что было сделано с учётом метода WriteLine, вызываемого в конце программы, которому требуется передать именно это количество аргументов.

2002-2013 (c) wasm.ru