Від C до Асемблера, Програмування, Статті, Бібліотека Лінуксцентру, – експерт з Linux та
1. Короткий огляд
Що входить до складу мікрокомп'ютерних систем? Типова відповідь на це питання: "мікропроцесор, шина, підсистема пам'яті, підсистема введення-виводу та інтерфейсна частина, що поєднує всі компоненти воєдино".
Але це справедливо лише для апаратної складової. Будь-яка мікрокомп'ютерна система вимагає наявності програмного забезпечення (ПЗ), яке керуватиме апаратурою. Програмне забезпечення, у свою чергу, можна поділити на системне програмне забезпечення та прикладне.
До складу прикладного ПЗ можуть входити різноманітні бібліотеки у вигляді наборів підпрограм, необхідних для роботи програм.
Програми машинною мовою - це такі програми, які мікропроцесор "розуміє" без додаткової обробки. Мова асемблера складається з набору інструкцій, кожна з яких практично одна відповідає своїй машинній команді. Асемблерні інструкції записуються у вигляді мнемонік, які ближче до людської мови, ніж машинні коди і тому простіші для розуміння людиною. Мови високого рівня дуже близькі до природної англійської мови, а їх структурованість дозволяє програмісту легко та просто викладати свої думки. Однак, незалежно від того якою мовою була написана програма, чи то асемблер або високорівнева мова, її необхідно перетворити на машинний код за допомогою програми, яка називається транслятором. Найчастіше їх називають асемблер і компілятор, або інтерпретатор відповідно.
2. Початок
Для початку напишіть невелику програму мовою C, яка виводить на екран повідомлення hello world і скомпілюйте її з ключем -S. В результаті ви отримаєте файл з асемблерним кодом, що відповідає вихідній програмі. GCC створює за промовчаннямфайл з асемблерним кодом з тим самим ім'ям, що й вихідний файл, замінюючи розширення `.c' на `.s'. Спробуйте інтерпретувати кілька рядків у отриманому файлі.
Ця інструкція записує число 10 в регістр eax. Префікси '%', перед ім'ям регістра, і '$', перед числом, обов'язкові, бо того вимагає синтаксис асемблера. Слід зазначити, що не всі асемблери дотримуються однакового синтаксису.
Напишемо нашу першу програму мовою асемблера та збережемо її у файл first.s. Текст програми наводиться нижче.
Цю програму можна асемблювати і об'єднати в модуль a.out, що виконується, якщо дати команду cc first.s. Розширення імені файлу `.s' допомагає компілятору ідентифікувати мову, якою написана програма, в результаті cc викликає асемблер і лінковник, пропускаючи стадію компіляції.
3. Арифметичні операції, операції порівняння та цикли
Наступна програма обчислює факторіал числа, що знаходиться в регістрі eax. Результат обчислень зберігається у регістрі ebx.
Тут L1 і L2 - це позначки. Коли програма входить у точку L2, ebx містить факторіал числа, що у регістрі eax.
4. Підпрограми
При створенні складної програми ми зазвичай розбиваємо її на прості підзавдання. Для кожної з таких підзадач пишеться своя підпрограма чи функція. Після цього ми можемо викликати ту чи іншу функцію в міру потреби. УЛистингу 3 наводиться приклад створення та виклику підпрограм мовою асемблера.
Тут інструкція call передає керування підпрограмою foo. Інструкція ret, в підпрограмі foo, передає управління назад наступної інструкції, розташованої за інструкцією call.
Як правило, функції визначають свої набори локальних змінних та вхідних аргументів,використовуються під час кожного виклику. Для цих змінних необхідно виділяти області пам'яті, зазвичай цих цілей використовується стек. Дуже важливо розуміти основні принципи виділення місця на стеку під локальні змінні та аргументи, як ці змінні ініціалізуються та як вони використовуються при повторному, рекурсивному чи іншому способі виклику функції під час виконання програми. Робота з регістрами esp/ebp та використання інструкцій роботи зі стеком push/pop є центральним аспектом, знання якого обов'язково для розуміння механізму виклику підпрограм та повернення в точку виклику.
5. Робота зі стеком
Інструкція popl %eax скопіює число з вершини стека (чотири байти) в регістр eax і збільшить вміст регістру esp на чотири. А якщо вам потрібно просто "викинути" число з вершини стека, нікуди його не копіюючи? Для цього можна просто виконати інструкцію addl $4 %esp, яка просто збільшить вміст регістру покажчика стека на чотири.
6. Виділення місця на стеку під локальні змінні
Програми на мові C можуть включати сотні і тисячі змінних. Асемблерний лістинг, еквівалентний програмі мовою C, допоможе вам зрозуміти як ці змінні розміщуються і як використовуються регістри процесора, при роботі зі змінними.
Кількість регістрів в процесорі не так багато і вони не можуть бути використані для зберігання всіх змінних, що є в програмі. Локальні змінні розміщуються на стеку.Листинг 4 демонструє - як це робиться.
6. Вхідні аргументи та значення, що повертаються
Стек може використовуватися для передачі підпрограму значень вхідних аргументів. Дотримуватимемося угод, прийнятих у мові C, під час передачі вхідних параметрів у підпрограму. Відповідно доякими, регістр eax служить повернення результату в викликаючу програму, а вхідні аргументи передаються підпрограмі через стек.Листинг 5 демонструє виклик простої функції sqr, яка приймає один аргумент.
8. Комбінування програм на мові C та асемблері
УЛистингу 6 представлені програма мовою C і модуль функції, написаної мовою асемблера. Програма мовою C перебуває у файлі main.c, а асемблерний модуль із текстом функції -- у файлі sqr.s. Щоб скомпілювати та злінкувати таку програму, дайте команду cc main.c sqr.s.
Зворотний варіант (виклик C-шної функції асемблера) не менш простий і зрозумілий. УЛистингу 7 демонструється можливість виклику функції, написаної мовою C, із програми, написаної мовою асемблера.
9. Асемблерний код, що генерується компілятором GNU C
Виклик функції add з аргументами 10 і 20 буде відтрансльований в наступний асемблерний код:
Зверніть увагу: останній аргумент міститься на стек першим.
10. Глобальні змінні
Ми знаємо, що простір під локальні змінні виділяється на стеку простим зменшенням вмісту регістру esp. А як виділяється простір під глобальні змінні? Відповідь на це питання ви знайдете вЛістингу 9.
11. Системні виклики
Якщо програма не просто здійснює якісь арифметичні обчислення мовою асемблера, а також організує введення/виведення даних тощо, то їй не обійтися без виклику служб операційної системи. Насправді, якщо не торкатися системних викликів, то програмування на асемблері практично не залежить від типу операційної системи.
Існує два загальноприйняті способи виконання системних викликів уLinux: через бібліотеку libc та безпосередньо.
Libc виконує роль захисного прошарку, оберігаючи програму від можливих помилок на той випадок, якщо в ядрі зміниться синтаксис того чи іншого системного виклику та надає POSIX-сумісний інтерфейс із ядром. Однак, ядро Linux саме по собі є більш менш POSIX-сумісним, це означає, що синтаксис виклику бібліотечних функцій-оберток з libc точно збігається з синтаксисом реальних системних викликів ядра (і навпаки).
Розглянемо лістинг, поданий нижче.
Скомпілюйте програму командою cc-g fork.c-static. Запустіть gdb та дайте команди file fork disassemble fork.
(Прим.ред. -- по-перше, для cc необхідно додати ключ-oі в цьому випадку команда збірки fork.c буде виглядати так:cc -o fork -g fork.c -staticАбо, запустивши gdb, доведеться вказувати неfile fork, аfile a.out, тому що за відсутності ключа-oлінкується програма з ім'ямa.out. Це стосується і інших прикладів.)
Перед вами з'явиться асемблерний листинг програми, звідки ви побачите, як виконується системний виклик fork. Ключ -static - це ключ статичного лінкування GCC (див. сторінки довідкового посібника man gcc). Спробуйте зробити те саме з іншими системними викликами.
11. Програмування на вбудованому асемблері
Компілятор GNU C надає можливість вставляти асемблерний код прямо в текст програми мовою C. Само собою зрозуміло, що асемблерні інструкції, що використовуються, залежать від архітектури.
Для вставлення асемблерного коду використовується інструкція asm, наприклад:
що для процесорів сімейства x86 відповідає виразу мовою C:
Ви можете помітити, що на відміну відзвичайного асемблера, інструкція asm допускає вказівку вхідних та вихідних аргументів із використанням синтаксису мови C. Не слід бездумно користуватися інструкцією asm. Але тоді навіщо нею користуватись взагалі?
- Інструкція asm дозволить програмі отримати прямий доступ до апаратури комп'ютера. Це може підвищити швидкість виконання програм. Її можна використовувати для написання коду, який увійде до складу операційної системи і який взаємодіятиме з апаратурою комп'ютера. Наприклад, /usr/include/asm/io.h містить асемблерний код для прямого доступу до портів вводу/виводу.
- Асемблерні вставки допоможуть значно підняти швидкість проходження глибоких вкладених циклів у програмі. Наприклад, sine і cosine для одного й того ж значення кута можна замінити однією асемблерною інструкцією fsincos. Можливо два лістинги, наведені нижче, допоможуть вам краще зрозуміти важливість фактора часу.
Скомпілюйте ці два приклади з ключами оптимізації, як показано нижче:
Для вимірювання швидкості виконання кожної версії скористайтесь командою time і задайте в командному рядку досить велике число, щоб кожна версія відпрацювала принаймні кілька секунд.
Результати будуть дещо відрізнятися для різних машин, проте ви напевно помітите, що версія програми, яка використовує асемблерну вставку, працює набагато швидше.
Оптимізатор GCC намагається переупорядкувати та переписати код програми з метою мінімізації часу виконання, навіть у тому випадку, коли у програмі є асемблерні вставки. Коли оптимізатор виявляє, що результат виконання asm-інструкції ніде не використовується, він може просто виключити її з тексту програми, якщо між інструкцією asm і її операндамивідсутня ключове слово volatile (як окремий випадок, gcc не переміщає асемблерні вставки, що не повертають результат виконання, за межі циклу). Будь-яка, окремо взята, асемблерна вставка може бути переміщена зі свого місця і вкрай важко вгадати заздалегідь, як нею розпорядиться оптимізатор. Єдина можливість зберегти порядок дотримання асемблерних інструкцій - це вставити весь блок асемблерного коду в одну інструкцію asm.
Використання вказівок asm може знизити ефективність оптимізатора, оскільки компілятор нічого не знає про семантику asm. У цьому випадку GCC змушений перейти до більш консервативного режиму прогнозування, що може призвести до відмови деяких видів оптимізації.
12. Вправи
- Розберіться з асемблерним кодом програми на мові C з Лістингу 6. Змініть його таким чином, щоб більше не з'являлися попереджувальні повідомлення, що виникають при генерації асемблерного коду з ключем -Wall. Порівняйте два отримані асемблерні лістинги. Які зміни ви помітили?
- Скомпілюйте якусь невелику програму на мові C з оптимізацією (наприклад -O2) і без неї. Подивіться асемблерний код, що вийшов, і знайдіть основні відмінності, виконані компілятором.
- Розберіть асемблерний код, який генерується оператором вибору switch.
- Скомпілюйте маленьку програму на мові C, яка має асемблерну вставку. Які відмінності ви помітили в асемблерному коді такої програми?
- Вкладені функції - це такі функції, які визначаються в тілі іншої ("охоплюючої") функції, таким чином:
- вкладена функція має можливість доступу до локальних змінних "охоплюючої" функції та
- вкладені функції є локальними запо відношенню до "охоплюючих" функцій і не можуть бути викликані за межами "охоплюючих" функцій, якщо вони не передадуть вам покажчик на вкладену функцію.
Розберіть програму, наведену нижче:
Я щойно склав іспити за останній курс Урядового Коледжу Комп'ютерних Наук у місті Трікур (Trichur), Індія, штат Kerala.
Hiran Ramankutty. Переклад: Андрій Кисельов - Від C до Асемблера Версія для друку