П р о г р а м м и р о в а н и е

в

L I N U X

Программирование в LINUX (первый пример).

Си-модуль:
#include < stdlib.h >
#include < stdio.h >

extern void asm_calc(int,int,int*,int*);
int main(void){
int i,j,iplusj=0,imulj=0;
scanf("%d %d",&i,&j);
printf("C :\t i+j=%d\n",i+j);
printf("C :\t i*j=%d\n",i*j);
asm_calc(i,j,&iplusj,&imulj);
printf("ASM:\t i+j=%d\n",iplusj);
printf("ASM:\t i*j=%d\n",imulj);
return 0;
};


ASM-модуль:

SECTION .TEXT
GLOBAL asm_calc
asm_calc:
; параметры: int i,int j, int* iplusj, int* imulj
; то есть - число 1,число 2, указатель на результат 1, указатель на результат 2
; по правилам вызова С - операнды в стеке.
; [esp] - адрес возврата
; [esp+4] - первый аргумент
; [esp+8] - второй и т.д.
PUSH EAX
PUSH ECX
PUSH EBX
PUSH EDX
MOV EAX, [ESP+4+4*4] ; i     4*4 - сохранённые в стек регистры
MOV EBX, [ESP+8+4*4] ; j
MOV ECX, [esp+16+4*4] ; imulj
MUL EBX ; Результат в edx:eax
MOV [ECX],EAX ; Результат перемножения заброшен по адресу в ecx

MOV ECX, [ESP+12+4*4]

; iplusj
MOV EAX, [ESP+4+4*4] ; i
MOV EBX, [ESP+8+4*4] ; j
ADD EAX,EBX
MOV [ECX],EAX

; Результат сложения заброшен туда же - по адресу в ecx

POP EDX
POP EBX
POP ECX
POP EAX
RET

makefile:
LDFLAGS=-g
CFLAGS=-g
all: main

main: main.o asm_file.o

main.o: main.c

asm_file.o: asm_file.asm
nasm -g -o $@ -f elf $^


Утилита make.

Задача утилиты make - автоматически определять, какие файлы проекта были изменены и требуют компиляции, и применять необходимые для этого команды. Хотя примеры применения относятся к использованию утилиты для описания процесса компиляции программ на языке С/С++, утилита может использоваться для описания сценариев обновления любых файлов.

Структура Makefile.

Мakefile состоит из так называемых "правил", имеющих вид:

имя-результата:     исходные-имена ...
команды
...
...


имя-результата - это обычно имя файла, генерируемого программой, например, исполняемый или объектный файл. "Результатом" может быть действие никак не связанное с процессом компиляции, например, clean - очистка.

исходное-имя - это имя файла, используемого на вводе, необходимое, чтобы создать файл с именем-результата.

команда - это действие, выполняемое утилитой make. Правило может включать более одной команды, В начале каждой команды надо вставлять отступ (символ "Tab"). Команда выполняется, если один из файлов в списке исходные-имена изменился. Допускается написание правила содержащего команду без указания зависимостей. Например, можно создать правило "clean", удаляющее объектные файлы проекта, без указания имен.

Итак, правила объясняют как и в каком случае надо пересобирать определённые файлы проекта.

Стандартные правила:
К числу стандартных правил относятся:

  • all - основная задача, компиляция программы.
  • install - копирует исполняемые коды программ, библиотеки настройки и всё что необходимо для последующего использования
  • uninstall - удаляет компоненты программы из системы
  • clean - удаляет из директории проекта все временные и вспомогательные файлы.

Пример Makefile:
Ниже приводится простой пример (номера строк добавлены для ясности).
# Создать исполняемый файл "client"
1 client: conn.o
2     g++ client.cpp conn.o -o client

# Создать объектный файл "conn.o"
3 conn.o: conn.cpp conn.h
4     g++ -c conn.cpp -o conn.o


В этом примере строка, содержащая текст client: conn.o, называется "строкой зависимостей", а строка g++ client.cpp conn.o -o client называется "правилом" и описывает действие, которое необходимо выполнить.

1 Задается цель -- исполняемый файл client, который зависит от объектного файла conn.o;
2 Правило для сборки данной цели;
3 Задается цель conn.o и файлы, от которых она зависит -- conn.cpp и conn.h;
4 Описывается действие по сборке цели conn.o.

Строки, начинающиеся с символа "#", являются комментариями.

"Ложная" цель:
Обычно "ложные" [phony] цели, представляющие "мнимое" имя целевого файла, используются в случае возникновения конфликтов между именами целей и именами файлов при явном задании имени цели в командной строке. Допустим в makefile имеется правило, которое не создает ничего, например:
clean:
rm *.o temp


Поскольку команда rm не создает файл с именем clean, то такого файла никогда не будет создано и поэтому команда make clean всегда будет срабатывать.

Декларация .PHONY:
Однако, данное правило не будет работать, если в текущем каталоге будет существовать файл с именем clean. Поскольку цель clean не имеет зависимостей, то она никогда не будет считаться устаревшей и, соответственно, команда 'rm *.o temp' никогда не будет выполнена. (при запуске make проверяет даты модификации целевого файла и тех файлов, от которых он зависит. И если цель оказывается "старше", то make выполняет соответствующие команды-правила ) Для устранения подобных проблем предназначена специальная декларация .PHONY, объявляющая "ложную" цель. Например:
.PHONY : clean

Таким образом мы указываем необходимость исполнения цели, при явном ее указании, в виде make clean вне зависимости от того существует файл с таким именем или нет.

Переменные.

Определить переменную в makefile вы можете следующим образом:
$VAR_NAME=value

В соответствии с соглашениями имена переменных задаются в верхнем регистре:
$OBJECTS=main.o test.o

Чтобы получить значение переменной, необходимо ее имя заключить в круглые скобки и перед ними поставить символ '$', например:
$(VAR_NAME)

В makefile-ах существует два типа переменных: "упрощенно вычисляемые" и "рекурсивно вычисляемые". В рекурсивно вычисляемых переменных все ссылки на другие переменные будут замещены их значениями, например:
TOPDIR=/home/tedi/project
SRCDIR=$(TOPDIR)/src


При обращении к переменной SRCDIR вы получите значение
/home/tedi/project/src.

Однако рекурсивные переменные могут быть вычислены не всегда, например следующие определения:
CC = gcc -o
CC = $(CC) -O2


выльются в бесконечный цикл. Для разрешения этой проблемы следует использовать "упрощенно вычисляемые" переменные:
CC := gcc -o
CC += $(CC) -O2

Где символ ':=' создает переменную CC и присваивает ей значение "gcc -o". А символ '+=' добавляет "-O2" к значению переменной CC.

Вывод на экран через INT 80H.

SECTION .TEXT
GLOGAL _start

_start:

MOV EAX,15
MOV EBX,20
MUL EBX
CMP EAX,300
JNZ error
all_ok:
MOV EDX,len1 ; third argument: message length
MOV ECX,msg1 ; second argument: pointer to message to write
MOV EBX,1 ; first argument: file handle (stdout)
MOV EAX,4 ; system call number (sys_write)
INT 0x80 ; call kernel
JMP exit1
error: MOV EDX,len2

; third argument: message length
MOV ECX,msg2 ; second argument: pointer to message to write
MOV EBX,1 ; first argument: file handle (stdout)
MOV EAX,4 ; system call number (sys_write)
INT 0x80 ; call kernel
JMP exit1
exit1:
MOV EBX,0 ; first syscall argument: exit code
MOV EAX,1 ; system call number (sys_exit)
INT 0x80 ; call kernel

SECTION .DATA

; section declaration

msg1 DB "All OK!",0xa
len1 EQU $ - msg1 ; length of our string
msg2 DB "Error!",0xa
len2 EQU $ - msg2

makefile:
all:main

main: main.o
ld -s -o main main.o

main.o:main.asm
nasm -f elf $^

clean:
rm main *.o

Вывод с помощью printf.

EXTERN printf
SECTION .TEXT

; section declaration

GLOBAL main
main:

; write our string to stdout
MOV EAX,15
PUSH EAX
PUSH DWORD msg
CALL printf
ADD ESP,8

; and exit
MOV EBX,0 ; first syscall argument: exit code
MOV EAX,1 ; system call number (sys_exit)
INT 0x80 ; call kernel

SECTION .DATA

; section declaration
msg DB "And the number in eax=%d",0xa,0x0

makefile:
LDFLAGS=-g
all:main

main: main.o

main.o:main.asm
nasm -g -f elf $^

clean:
rm main *.o

Вызов функций scanf и printf из Nasm.

Функции scanf и printf определены в библиотеке glibc. Эти функции можно указать в ассемблерной программе как внешние с помощью директивы EXTERN. Объектный файл получается стандартным образом. А вот при компоновке (линковке) необходимо указать библиотеку libc.so либо использовать для компоновки gcc, который, в отличие от ld по умолчанию компонует все объектные файлы с библиотекой libc.so

global _start

;Объявляем используемые внешние функции из libc
EXTERN exit
EXTERN puts
EXTERN scanf
EXTERN printf

;Сегмент кода:
SECTION .TEXT

;Функция main:
_start:

;Параметры передаются в стеке:
PUSH DWORD msg
CALL puts


;По конвенции Си вызывающая процедура должна
;очищать стек от параметров самостоятельно:

SUB ESP, 4

PUSH DWORD a
PUSH DWORD b
PUSH DWORD msg1
CALL scanf
SUB ESP, 12
MOV EAX, DWORD [a]
ADD EAX, DWORD [b]

PUSH eax
PUSH DWORD msg2

CALL printf
ADD ESP, 8


;Завершение программы с кодом выхода 0:
PUSH DWORD 0
CALL exit

RET


;Сегмент инициализированных данных
SECTION .DATA
msg : DB "An example of interfacing with GLIBC.",0xA,0
msg1 : DB "%d%d",0
msg2 : DB "%d", 0xA, 0


; Сегмент неинициализированных данных
SECTION .BSS
a RESD 1
b RESD 1

Арифметические операции в формате ASCII.

Данные, вводимые с клавиатуры, имеют ASCII-формат, например, цифры 1234 - шест.31323334. Для выполнения арифметических операций над числовыми значениями, такими как шест.31323334, требуется специальная обработка.

С помощью следующих ассемблерных команд можно выполнять арифметические операции непосредственно над числами в ASCII-формате:

  • AAA (ASCII Adjust for Addition - коррекция для сложения ASCII-кода)
  • AAD (ASCII Adjust for Division - коррекция для деления ASCII-кода)
  • AAM (ASCII Adjust for Multiplication - коррекция для умножения ASCII-кода)
  • AAS (ASCII Adjust for Subtraction - коррекция для вычитания ASCII-кода)

Эти команды кодируются без операндов и выполняют автоматическую коррекцию в регистре AX. Коррекция необходима, так как ASCII-код представляет так называемый распакованный десятичный формат, в то время, как компьютер выполняет арифметические операции в двоичном формате.

Сложение в ASCII-формате.

Рассмотрим процесс сложения чисел 8 и 4 в ASCII-формате:

Шест. 38
34
---
Шест. 6C

Полученная сумма неправильна ни для ASCII-формата, ни для двоичного формата. Однако, игнорируя левую 6 и прибавив 6 к правой шест. C: шест.C + 6 = шест.12 - получим правильный результат в десятичном формате. Правильный пример слегка упрощен, но он хорошо демонстрирует процесс, который выполняет команда AAA при коррекции.

В качестве примера, предположим, что регистр AX содержит шест.0038, а регистр BX - шест.0034. Числа 38 и 34 представляют два байта в ASCII-формате, которые необходимо сложить. Сложение и коррекция кодируется следующими командами:

ADD AL,BL     ; Сложить 34 и 38
AAA           ; Коррекция для сложения ASCII-кодов

Команда AAA проверяет правую шест. цифру (4 бита) в регистре AL. Если эта цифра находится между A и F или флаг AF равен 1, то к регистру AL прибавляется 6, а к регистру AH прибавляется 1, флаги AF и CF устанавливаются в 1. Во всех случаях команда AAA устанавливает в 0 левую шест. цифру в регистре AL. Результат - в регистре AX:

После команды ADD: 006C
После команды AAA: 0102

Для того, чтобы выработать окончательное ASCII-представление, достаточно просто поставить тройки на место левых шест. цифр:

OR AX,3030H ;Результат 3132

Все показанное выше представляет сложение однобайтовых чисел. Сложение многобайтных ASCII-чисел требует организации цикла, который выполняет обработку справа налево с учетом переноса. В примере складываются два трехбайтовых ASCII-числа в четырехбайтовую сумму. Обратите внимание на следующее:

CODESG SEGMENT
ASSUME CS:CODESG,DS:CODESG,SS:CODESG
ORG 100H
BEGIN: JMP SHORT MAIN
; ---------------------------------------------------------------
ASC1 DB '578' ; Элементы данных
ASC2 DB '694'
ASC3 DB '0000'
; ---------------------------------------------------------------
MAIN PROC NEAR
CLC
LEA SI,AASC1+2 ; Адреса ASCII-чисел
LEA DI,AASC2+2
LEA BX,AASC1+3
MOV CX,03 ; Выполнить 3 цикла
A20:
MOV AH,00

; Очистить регистр AH
MOV AL,[SI] ; Загрузить ASCII-байт
ADC AL,[DI] ; Сложение (с переносом)
AAA ; Коррекция для ASCII
MOV [BX],AL ; Сохранение суммы
DEC SI
DEC DI
DEC BX
LOOP A20 ; Циклиться 3 раза
MOV [BX],AH ; Сохранить перенос
RET
MAIN ENDP
CODESG ENDS
END BEGIN

В программе используется команда ADC, так как любое сложение может вызвать перенос, который должен быть прибавлен к следующему (слева) байту. Команда CLC устанавливает флаг CF в нулевое состояние.

Команда MOV очищает регистр AH в каждом цикле, так как команда AAA может прибавить к нему единицу. Команда ADC учитывает пеpеносы. Заметьте, что использование команд XOR или SUB для oчистки регистра AH изменяет флаг CF. Когда завершается каждый цикл, происходит пересылка содержимого pегистра AH (00 или 01) в левый байт суммы.
В результате получается сумма в виде 01020702. Программа не использует команду OR после команды AAA для занесения левой тройки, так как при этом устанавливается флаг CF, что изменит pезультат команды ADC. Одним из решений в данном случае является сохранение флагового регистра с помощью команды PUSHF, выполнение команды OR, и, затем, восстановление флагового регистра командой POPF:

ADC AL,[DI] ; Сложение с переносом
AAA ; Коррекция для ASCII
PUSHF ; Сохранение флагов
OR AL,30H ; Запись левой тройки
POPF ; Восстановление флагов
MOV [BX],AL ; Сохранение суммы

Вместо команд PUSHF и POPF можно использовать команды LAHF (Load AH with Flags загрузка флагов в регистр AH) и SAHF (Store AH in Flagregister - запись флагов из регистра AH во флаговый регистр). Команда LAHF загружает в регистр AH флаги SF, ZF, AF, PF и CF; а команда SAHF записывает содержимое регистра AH в указанные флаги. В приведенном примере, однако, регистр AH уже используется для арифметических переполнений. Другой способ вставки троек для получения ASCII-кодов цифр - организовать обработку суммы командой OR в цикле.

Вычитание в ASCII-формате.

Команда AAS (ASCII Adjust for Subtraction - коррекция для вычитания ASCII-кодов) выполняется aналогично команде AAA. Команда AAS проверяет правую шест. цифру (четыре бита) в регистре AL. Если эта цифра лежит между A и F или флаг AF равен 1, то из регистра AL вычитается 6, а из регистра AH вычитается 1, флаги AF и CF устанавливаются в 1. Во всех случаях команда AAS устанавливает в 0 левую шест.цифру в регистpе AL.

В следующих двух примерах предполагается, что поле ASC1 содержит шест.38, а поле ASC2 - шест.34:

Пример 1:    AX AF
MOV AL,ASC1 ; 0038
SUB AL,ASC2 ; 0034 0
AAS ; 0004 0

Пример 2:    AX AF
MOV AL,ASC2 ; 0034
SUB AL,ASC1 ; 00FC 1
AAS ; FF06 1

В примере 1 команде AAS не требуется выполнять коррекцию. В примере 2, так как правая цифра в регистре AL равна шест.C, команда AAS вычитает 6 из регистра AL и 1 из регистра AH и устанавливает в 1 флаги AF и CF. Результат (который должен быть равен -4) имеет шест. представление FF06, т.е. десятичное дополнение числа -4.

Умножение в ASCII-формате.

Команда AAM (ASCII Adjust for Multiplication - коррекция для умножения ASCII-кодов) выполняет корректировку результата умножения ASCII-кодов в регистре AX. Однако, шест. Цифры должны быть очищены от троек и полученные данные уже не будут являться действительными ASCII-кодами. (В руководствах фирмы IBM для таких данных используется термин pаспакованный десятичный формат). Например, число в ASCII-формате 31323334 имеет распакованное десятичное представление 01020304. Кроме этого, надо помнить, что коррекция осуществляется только для одного байта за одно выполнение, поэтому можно умножать только oднобайтные поля. Для более длинных полей необходима организация цикла.

Команда AAM делит содержимое регистра AL на 10 (шест.0A) и записывает частное в регистр AH, а остаток в AL. Предположим, что в регистре AL содержится шест.35, а в регистре CL - шест.39.

Следующие команды умножают содержимое регистра AL на содержимое CL и преобразуют результат в ASCII-формат:

AX:
AND CL,0FH ; Преобразовать CL в 09
AND AL,0FH ; Преобразовать AL в 05 0005
MUL CL ; Умножить AL на CL 002D
AAM ; Преобразовать в распак.дес. 0405
OR AX,3030H ; Преобразовать в ASCII-ф-т 3435

Команда MUL генерирует 45 (шест.002D) в регистре AX, после чего команда AAM делит это значение на 10, записывая частное 04 в регистр AH и остаток 05 в регистр AL. Команда OR преобpазует затем распакованное десятичное число в ASCII-формат.

Следующий пример демонстрирует умножение четырехбайтового множимого на однобайтовый множитель.

Так как команда AAM может иметь дело только с однобайтовыми числами, то в программе организован цикл, который обрабатывает байты справа налево. Окончательный результат умножения в данном примере - 0108090105.

Если множитель больше одного байта, то необходимо обеспечить еще один цикл, который обрабатывает множитель. В этом случае проще будет преобразовать число из ASCII-формата в двоичный формат.

CODESG SEGMENT
ASSUME CS:CODESG,DS:CODESG,SS:CODESG
ORG 100H
BEGIN: JMP MAIN

; ---------------------------------------------------------------
MULTCND DB '3783' ; Элементы данных
MULTPLR DB '5'
PRODUCT DB 5 DUP(0)

; ---------------------------------------------------------------
MAIN PROC NEAR
MOV CX,04 ; 4 цикла
LEA SI,MULTCND+3
LEA DI,PRODUCT+4
AND MULTPLR,0FH ; Удалить ASCII-тройку
A20:
MOV AL,[SI] ; Загрузить ASCII-символ
AND AL,OFH ; Удалить ASCII-тройку
MUL MULTPLR ; Умножить
AAM ; Коррекция для ASCII
ADD AL,[DI] ; Сложить с
AAA ; записанным
MOV [DI],AL ; произведением
DEC DI
MOV [DI],AH ; Записать перенос
DEC SI
LOOP A20 ; Циклиться 4 раза
RET
MAIN ENDP
CODESG ENDS
END BEGIN

Деление в ASCII-формате.

Команда AAD (ASCII Adjust for Division - коррекция для деления ASCII-кодов) выполняет корректировку ASCII-кода делимого до непосредственного деления. Однако, прежде необходимо очистить левые тройки ASCII-кодов для получения распакованного десятичного формата. Команда AAD может оперировать с двухбайтовыми делимыми в регистре AX. Предположим, что регистр AX содержит делимое 3238 в ASCII-формате и регистр CL содержит делитель 37 также в ASCII-формате.

Следующие команды выполняют коррекцию для последующего деления:

AND CL,0FH ; Преобразовать CL в распак.дес.
AND AX,0F0FH ; Преобразовать AX в распак.дес. 0208
AAD ; Преобразовать в двоичный 001C
DIV CL ; Разделить на 7 0004

Команда AAD умножает содержимое AH на 10 (шест.0A), прибавляет pезультат 20 (шест.14) к регистру AL и очищает регистр AH. Значение 001C есть шест. представление десятичного числа 28. Делитель может быть только однобайтовый от 01 до 09.

CODESG SEGMENT
ASSUME CS:CODESG,DS:CODESG,SS:CODESG
ORG 100H
BEGIN: JMP SHORT MAIN

; ---------------------------------------------------------------
DIVDND DB '3698' ; Элементы данных
DIVSOR DB '4'
QUOTNT DB 4 DUP(0)

; ---------------------------------------------------------------
MAIN PROC NEAR
MOV CX,04 ; 4 цикла
SUB AH,AH ; Стереть левый байт делимого
AND DIVSOR,0FH ; Стереть ASCII 3 в делителе
LEA SI,DIVDND
LEA DI,QUOTNT
A20:
MOV AL,[SI] ; Загрузить ASCII байт
; (можно LODSB)
AND AL,0FH ; Стереть ASCII тройку
AAD ; Коррекция для деления
DIV DIVSOR ; Деление
MOV [DI],AL ; Сохранить частное
INC SI
INC DI
LOOP A20 ; Циклиться 4 раза
RET
MAIN ENDP
CODEGS ENDS

ДВОИЧНО-ДЕСЯТИЧНЫЙ ФОРМАТ (BCD).

В предыдущем примере деления в ASCII-формате было получено частное 00090204. Если сжать это значение, сохраняя только правые цифры каждого байта, то получим 0924. Такой формат называется двоично-десятичным (BCD - Binary Coded Decimal) или упакованным. Он содержит только десятичные цифры от 0 до 9. Длина двоично-десятичного представления в два раза меньше ASCII-представления.
Заметим, однако, что десятичное число 0924 имеет основание 10 и, будучи преобразованным в основание 16 (т.е. в шест. представление), даст шест.039C.
Можно выполнять сложение и вычитание чисел в двоично-десятичном представлении (BCD-формате). Для этих целей имеются две корректиpующих команды:

DAA (Decimal Adjustment for Addition - десятичная коррекция для сложения)
DAS (Decimal Adjustment for Subtraction - десятичн. коррекция для вычит.)

Обработка полей также осуществляется по одному байту за одно выполнение. В примере программы выполняется преобразование чисел из ASCII-формата в BCD-формат и сложение их.

Процедура B10CONV преобразует ASCII в BCD. Обработка чисел может выполняться как справа налево, так и слева направо. Кроме того, обработка слов проще, чем обработка байтов, так как для генерации одного байта BCD-кода требуется два байта ASCII-кода. Ориентация на обработку слов требует четного количества байтов в ASCII-поле.

Процедура C10ADD выполняет сложение чисел в BCD-формате. Окончательный результат - 127263.

CODESG SEGMENT PARA "Code"
ASSUME CS:CODESG,DS:CODESG,SS:CODESG
ORG 100H
BEGIN: JMP SHORT MAIN
; ---------------------------------------------------------------
ASC1 DB '057836'
ASC2 DB '069427'
BCD1 DB '000'
BCD2 DB '000'
BCD3 DB 4 DUP(0)
; ---------------------------------------------------------------
MAIN PROC NEAR
LEA SI,ASC1+4 ; Инициализировать для ASC1
LEA DI,BCD1+2
CALL B10CONV ; Вызвать преобразование
LEA SI,ASC2+4 ; Инициализировать для ASC2
LEA DI,BCD2+2
CALL B10CONV ; Вызвать преобразование
CALL C10ADD ; Вызвать сложение
RET
MAIN ENDP


; Преобразование ASCII в BCD:
; ---------------------------------------------------------------

B10CONV PROC
MOV CL,04 ; Фактор сдвига
MOV DX,03 ; Число слов
В20:
MOV AX,[SI] ; Получить ASCII-пapy
XCHG AH,AL
SHL AL,CL ; Удалить тройки
SHL AX,CL ; ASCII-кода
MOV [DI],AH ; Записать BCD-цифру
DEC SI
DEC SI
DEC DI
DEC DX
JNZ В20
RET
B10CONV ENDP


; Сложение BCD-чисел:
; ---------------------------------------------------------------

C10ADD PROC
XOR AН,AН ; 0чистить AН
LEA SI,BCD1+2 ; Инициализация
LEA DI,BCD2+2 ; BCD
LEA BX,BCD3+3 ; адресов
MOV CX,03 ; Трехбайтные поля
CLC
С20:
MOV AL,[SI] ; Получить BCD1 (или LODSB)
ADC AL,[DI] ; Прибавить BCD2
DAA ; Десятичная коррекция
MOV [BX],AL ; 3аписать в BCD3
DEC SI
DEC DI
DEC BX
LOOP С20 ; Цикл 3 раза
RET
C10ADD ENDP

CODESG ENDS
END BEGIN

Лабораторная работа №3

Программирование на Машинно-Ориентированных Языках.
Преподаватель: Коробов С.А.