Перейти к основному содержимому

Язык программирования С

· 5 мин. чтения

Все программы на assembly пишутся под определенный процессор. К примеру, у одного процессора есть регистр eax, а у другого - нет. Или новый процессор умеет высчитывать sin от числа, а старые - нет. Программа, написанная с использованием регистра eax или специфической команды, будет работать только на определенных процессорах. Брайен Керниган и Деннис Ричи решили создать язык программирования способный решить эту проблему. Так появился язык Си. Синтаксис языка напрямую не связан с командами процессора. Однако, под каждый новый процессор создается компилятор, который транслирует код программы в бинарный код этого процессора. Таким образом, единожды написанная программа на языке Си может быть скомпилирована в бинарный файл для любого современного процессора для которого есть компилятор! Компилятор языка Си - это программа, которая преобразует файл с исходным кодом на языке Си в бинарный файл. Как и с assembly, компиляторов языка Си достаточно много. Каждый из них имеет особенности. Но говоря о языке Си, мы говорим о стандарте языка, который все компиляторы должны выполнять. Все начинается с функции Если в языке assembly мы тупо пишем последовательность команд, часть из них мы можем помечать как функции, то в языке Си все начинается с функций. Вся программа состоит из взаимодействующих функций, в которых мы помещаем последовательность команд или инструкций. Инструкции называть их будет точней, т.к. одна инструкция может быть преобразована в несколько команд процессора. Любая программа должна содержать в себе функцию под названием main:

Как и в assembly, очень часто в функцию мы хотим передать какие-то данные и получить результат их обработки. В Си, входные параметры функции описываются внутри круглых скобок ( ), а выходное значение перед названием (в нашем примере void). Все инструкции, которые необходимо выполнить данной функции, описываются между фигурными скобками { }. В примере выше, функция имеет название main, возвращает void - т.е. ничего, не имеет входных параметров, и не содержит инструкций, т.е. ничего не делает. Давайте скомпилируем этот код в assembly:

Мы видим метку _main, в конце функции команду ret - возврат из функции. Между этими командами, вставлены еще несколько команд, которые вставляются по умолчанию в любую функцию: push rbp и pop rbp - сохраняем в стеке регистр rbp, т.к. далее хотим его использовать, а после завершения функции, восстанавливаем его значение до вызова. mov rbp, rsp - сохраняем значение rsp - регистра указателя стека, чтобы удобней было работать с данными переданными функции через стек. Почему не работать с ними через регистр rsp? Т.к. сама функция может использовать стек, значение rsp может менятся. А доступ к данным проще считать от началоного адреса.

Давайте в нашу программу добавим еще одну функцию с именем summarize:

В функцию main мы добавили инструкцию, вызов функции c именем summarize. Для вызова функции, мы просто пишем ее имя, после которого ставим круглые скобки. Все инструкции в языке Си заканчиваются ;. Компилируется это в следующий код на assembly:

Все довольно предсказуемо: добавился вызов функции call _summarize. Наша новая функция summarize, ничего не делает. Давайте передавать ей два числа для их сложения:

При вызове, в круглых скобках, через запятую, мы перечислили два числа. А при объявлении входных параметров добавили int a, int b. int - это тип данных, который в 8- и 16-разрядных процессорах соответствовал 2 байтам, а в более современных 32- и 64-разрядных системах соответствует 4 байтам. В данном случае - 32 бита или 4 байта. А вот a и b - это названия переменных, в которых будут лежать входные значения, при выполнении функции. В теле функции summarize мы добавили две инструкции. int result; - это объявление переменной размером int, т.е. места для хранения значения величиной в 4 байта. result - это имя переменной. Везде где мы будем писать имя переменной result, компилятор будет адресовать к значению к ячейке памяти, под которой подразумевается result. Инструкция result = a + b; - подразумевает сложение чисел a и b, а результат этого сложения нужно поместить в result. Вот как это выглядит на языке assembly:

Что мы тут видим: сначало mov edi, 3 - помещаем число 3 в регистр edi, затем mov esi, 6 - число 6 в регистр esi. после call _summarize - вызываем функцию summarize в функции summarize: mov dword [rbp-4H], edi - сохраняем 32 байта (это наш int) по адресу [rbp-4] (число, хранимое в регистре rbp - 4) из регистра edi. Видно, что у переменной а, адрес равен rbp-4. mov dword [rbp-8H], esi - это сохранение входного значения в переменную b. mov eax, dword [rbp-4H] - сохраняем значение переменной a в регистр eax. add eax, dword [rbp-8H] - складываем eax с переменной b. mov dword [rbp-0CH], eax - результат сохраняем по адресу [rbp-0C], это наша переменная result. Круто, но хотелось бы чтобы наша функция возвращала результат:

Для этого, вместо void, пишем возвращаемый тип. В нашем случае это int, т.е. 4 байта. Специальная инструкция return говорит какое значение необходимо вернуть функции. А чтобы получить возвращаемое из функции значение, в main и объявляем переменную result и инструкцией result = summarize(3, 6); - указываем, что в result нужно сохранить то, что вернет функция summarize. На assembly это выглядит так:

сохраняем 3 и 6 в регистры edi и esi. Вызываем функцию summarize из регистров edi и esi сохраняем значения в переменных a и b. складываем a и b, результат сохраняем в регистре eax. Выходим из функции mov dword [rbp-4H], eax - сохраняем в переменной result, то что хранится в регистре eax. Видно, что result - это ячейка памяти с адресом rbp-4 Программируя под операционную систему, функция main должна обязательно возвращать какое-то значение типа int. 0 для операционной системы означает, что программа завершилась корректно, любое другое число - с ошибкой. Подкорректируем нашу программу:

Типы данных С типом int мы уже познакомились. Это простые числа. Есть еще float и char. float - это числа с плавающей запятой.

char - это символ. Но мы же знаем, что символ в памяти компьютера, это просто число. Именно поэтому мы может применять математические операции к этому типу.

//TODO