itkvariat

    Секреты машинного кода



    В одной из прошлых статей мы поговорили о языках программирования высокого уровня. Сегодня мы опустимся гораздо ниже — в самые недра программных систем и попробуем разобраться в том, на каком «языке» говорит с пользователями и программистами сама ЭВМ.


    Двоичный код


    Мы часто вкладываем в это понятие самые различные смыслы. «Двоичный код», «машинный код», «программный код». Между тем, значение этих терминов различно. А вот использование в машинной математике двоичного кода очевидно и вполне логично.

    Цифровая электроника работает с двумя типами электрических сигналов, или, если точнее, с двумя «состояниями» — высоким уровнем сигнала  в его источнике либо передатчике и низким его уровнем. Высокий уровень сигнала — это «ноль», низкий — «единица». Это наиболее надежная схема передачи сигналов, которая дает стабильный результат. Какие бы помехи не возникали на пути сигнала, мы всегда поймем, где «единица», а где — «ноль».


    На такую схему великолепно ложится двоичная система счисления. Справедливости ради стоит отметить, что впервые такая система была описана в древнекитайской «книге Перемен». В дальнейшем двоичная система счисления только обретала теоретическую основу. Математик Лейбниц, например, доказал, что основой системы счисления может быть любое число, кроме нуля. Были, а, возможно и будут и другие теоретические работы в этом направлении.  Сейчас используются, как минимум, три системы счисления — двоичная, восьмеричная, шестнадцатиричная. Но нас интересует именно двоичная.


    Как работает «двоичный код»


    С помощью этого кода можно легко перевести десятичное число в понятное компьютеру двоичное. Возьмем число (например, 11001). Пронумеруем его с конца, причем, нумерацию начнем с нуля. Порядковый номер каждой цифры станет степенью, в которую мы возведем основание системы счисления (2), затем перемножим результат с цифрой. Нужно провести эту операцию с каждой цифрой двоичного числа, затем суммировать результат:


    1*(2^4)+1*(2^3)+10*(2^2)+0*(2^1)+1*(2^0)
    16+8+0+0+2 = 25

    Теперь попробуем перевести число 25 обратно, в двоичный код. Используем для этого простейшую арифметическую операцию — получение остатка от деления.


    25/2 =  1
    12/2 =  0
    6/2   =  0
    3/2   =  1
    1/2   =  1


    В результатах, снизу вверх, читаем наше число.

    Таким образом считать довольно легко, причем, даже на «пальцах». У нас ведь на руке пять пальцев? Если каждому присвоить единицу или ноль (ноль – палец согнут, единица – выпрямлен) и прикинуть значение основания системы, возведенной в нужную степень, то можно считать от нуля и до 1023. Правда, в случае с числом 25 жест руки будет немного неприличным.

    Для любителей посчитать есть сегодня любые инструменты — начиная от калькулятора и заканчивая таблицами разрядности.

    С дробными числами расчет ведется по иному. Всем цифрам до «запятой», степень присваивается в обратном порядке, от нуля, как и обычно, а вот числа после «запятой» сопровождаются отрицательной степенью, причем, она уменьшается от запятой. Вот как это выглядит:


    Число: 

    1101,101
    1*(2^3)+1*(2^2)+0*(2^1)+1*(2^0)+1*(2^-1)+0*(2^-2)+1*(2^-3)
    8+4+0+1 + 0,5+0+0,125 = 13,625

    В остальном, операция выполняется также.


    Как считает компьютер?


    Компьютер не может, допустим, приписать «минус» к двоичному числу и считать его отрицательным. Этот минус нужно где-то поместить. Простейший способ — снабдить число дополнительным знаком (в восьмиразрядной ячейке, в этом случае мы сможем поместить семиразрядное двоичное число, где дополнительный разряд будет означать знак — единица отрицательный, ноль — положительный). Это называется «прямым кодом».


    0111010
    0111010

    Но отрицательные числа лучше представлять по-другому — для того, чтобы компьютер мог с ними работать. «Единица» в крайнем разряде по-прежнему будет выполнять роль «маркера» отрицательного числа. Но остальные разряды требуется инвертировать (то есть, разряды с нулями заменить единицей и наоборот). На месте остается только маркер.


    1000101

    Теперь к числу прибавляем единицу:


    1  1000110

    Для чего же вся эта малопонятная морока? – спросит читатель. Для того, чтобы ЭВМ смогла выполнять операции с числами.


    0 0111010+0 0111010 = 1110100   (58+58 = 116)

    Знаковые разряды в этом случае просто не учитываются.

    Но если число отрицательное — все меняется. Переводим его в дополнительный или обратный код (инвертируем все разряды кроме знакового, затем прибавляем единицу) и складываем снова, затем из результата вычитаем единицу и инвертируем его:

    1  1000110+1 1000110  = 1 0001011 = 1 1110100 (-116)

    Вычитание работает по тому же принципу, умножение — путем последовательного сложения (я очень сильно упрощаю но принцип именно таков). Что касается операции деления, то она реализуется путем неоднократного сложения делимого с дополнительным кодом делителя до получения необходимого результата.

    Подробно эти действия  описывать не буду. Расскажу лишь про арифметический сдвиг. Это ещё одна «хитрость», которая позволяет упростить вычисления. Если сдвинуть двоичное число вправо или влево, то оно меняется, согласно определенным правилам.


    1111101 = 125

    Сдвиг вправо (слева добавляем единицу, последний разряд убираем):


    1111110 = 126

    Ещё один сдвиг вправо:


    1111111 = 127

    С дополнительным кодом — ещё интереснее. Сдвиг числа влево дает умножение на 2, вправо – деление на 2. Это очень удобно для умножения и деления целых чисел на числа, равные степени 2 (2, 4, 8, 16, 32, 64). Существует множество вариантов сдвига — арифметический (мы его выполнили), циклический, логический и т.д. Каждый выполняет свою задачу.

    Добавлю, что всё, о чем я говорил — это «азы» компьютерных вычислений. Когда-то они выполнялись компьютером именно так. Сегодня они ведутся уже на ином уровне. Персональный компьютер выполняет расчеты при помощи встроенного в ядро основного ЦПУ математического сопроцессора, где каждая операция выполняется одной командой, что значительно ускоряет работу. Математический сопроцессор оптимизирован именно для вычислений, эти функции заложены в него на аппаратном уровне. Но описание работы сопроцессора — это тема для отдельной статьи или даже целой книги.


    Кодируем… двоичный код


    Сейчас мы подошли к самому главному моменту: мы уже знаем, что компьютер выполняет операции с числами в двоичном коде. Причем, это единственный пока вариант для ЭВМ и не такой уж сложный, если вдуматься, для нашего с вами понимания. Остается выяснить, как же объяснить компьютеру, что он должен делать с числом (числами), чтобы получить результат.

    Для этого, очевидно, нужны команды, которые будут точно поняты и интерпретированы центральным процессором. А поскольку ничего, кроме двоичных чисел ЭВМ не понимает, то команды должны состоять из определенного набора цифр — машинной инструкции. Она представляет собой запись, состоящую из нескольких элементов — начального и конечного маркеров и самого её «тела».


    Машинные инструкции разрабатываются для каждого семейства процессоров и некоторых отдельных их разновидностей. Поддержка инструкций заложена в самом процессоре.

    Конечно, программу из таких инструкций написать очень и очень трудно, так как ручное управление процессором — задача нетривиальная и трудоемкая, для этого существуют специальные языки нижнего уровня, которые работают напрямую с процессором — ассемблеры. Специальная программа компилятор, превращает команды ассемблеров в машинные инструкции, которые располагаются в специальных бинарных (двоичных) файлах.

    Коды, коды, коды

    В принципе, кодирование информации происходит в компьютере неоднократно. Это циклический процесс, реализованный и на программном и на аппаратном уровнях. Начинается он со скан-кода (в IBM-совместимых компьютерах), с помощью которого драйвер клавиатуры распознает нажатия и отпускания клавиш.

    Далее в электронных документах, с которыми мы работаем, символы кодируются при помощи кодовых таблиц (кодировок) — (КОИ - 8, СР1251, СР866, Мас, ISO, Unicode), в которых на один символ отводится от одного и более байт. Коды у одинаковых символов в разных кодировках различны. Допустим, двоичное число 11000010 в кодировке KOI-8 будет означать строчную букву «б», а в кодировке CP1251 — прописную «В».

    С помощью стандартных кодировок записываются и команды абсолютно всех языков программирования. А затем программа проходит через транслятор, который снова кодирует её, только уже в понятные процессору машинные инструкции либо в ассемблер. В принципе, транслятор решает массу задач но это — тема для другого разговора. Наша цель — составить более-менее стройную картину преобразования информации внутри ЭВМ.

    Ассемблеры

    Собственно, мы уже несколько раз упоминали это «семейство» низкоуровневых языков программирования. Сейчас их используют реже, чем это было раньше, так как появилось немало более удобных, высокоуровневых языков. Тем не менее, ассемблеры всё же применяются — когда нужен небольшой но высокоэффективный фрагмент программного кода. Ассемблерные вставки поддерживаются и в ряде высокоуровневых языков программирования, с целью ускорения работы кода.

    Ассемблеры — аппаратно-ориентированные языки, они привязаны к архитектуре процессора, поэтому, их синтаксис для разных процессоров будет различен. Но их объединяет один принцип. Команды процессора, которые представляют собой просто числа, в ассемблерах представлены как «мнемонические» символы. Чтобы не расшифровывать это понятие, я просто продемонстрирую несколько небольших программ.

    Вот как будет выглядеть программа на ассемблере 8086 (x86-совместимая), которая выводит на консоль Hello World:

    use16                     
    org 100h                
     
        mov dx,hello      
        mov ah,9                    
        int 21h               
     
        mov ax,4C00h   
        int 21h              
    hello db 'Hello, world!$'
    

    Понять, что здесь написано не так уж и трудно. Use16 – генерация 16-битного кода, регистр DX (mov dx) содержит адрес строки (строка заканчивается значком $ и объявляется директивой db), регистр AH (mov ah) содержит 9 - номер функции DOS, int 21h — обращение к функциям DOS. 4C00h — завершение программы, где 0 — успешное её завершение.
    Может даже показаться, что программирование на ассемблере — легкая задача. Но это отнюдь не так. Если вы начнете делать что-то более или менее сложное, писанины будет куда больше. Вот, взгляните на вывод латинских букв по алфавиту в цикле:  
         
    use16                
    org 100h             
     
        mov ah,02h      
        mov dl,'A'       
        mov cx,26        
    tt:
        int 21h         
        inc dl        
        loop tt   
     
        mov ah,09h      
        mov dx,press     
        int 21h        
     
        mov ah,08h   
        int 21h        
     
        mov ax,4C00h     
        int 21h          
    press:
        db 13,10,'Press any key...$'

    Кроме уже знакомых нам команд здесь есть и другие. 02h — вывод символа, dl — первый символ, Inc dl – следующий символ, 08h — ввод без отображения (чтобы не закрылась программа), 13,10 — возврат каретки, перевод строки, сх — счетчик повторений. Loop — метка, которая «выбрасывает» цикл к началу.Собственно, далеко не каждый, даже опытный программист, сможет работать с ассемблерами. Для этого нужно очень хорошо знать архитектуру процессора. Для промышленного программирования гораздо удобнее пользоваться языками высокого уровня, о которых мы говорили в прошлый раз.

    Уровни общения

    Команды ассемблера — это, практически, прямые инструкции для процессора, которые выполняются и трактуются однозначно. Машинный код программ, написанных на современных языках программирования высокого уровня, генерируется и оптимизируется автоматически, порой проходя несколько уровней кодирования. Это сделано для того, чтобы ускорить и упростить работу программиста. Но «упрощение», в данном случае — весьма относительное понятие.

    Современные языки программирования представляют собой сложнейшие системы, которые позволяют относительно быстро создавать и тестировать программные комплексы любого масштаба и сложности.



    Подписывайтесь и читайте новости от ITквариат раньше остальных в нашем Telegram-канале !

    Поделитесь этой новостью с друзьями!

    Эдуард ТРОШИН

    Компьютерная газета

    Заметили ошибку? Выделите ее мышкой и нажмите Ctrl+Enter!  

    И еще на эту тему...
  • LG G6: большой тест - обзор
  • 11 секретов профессиональных фотографов из National Geogaphic
  • МЭСМ – наша точка отсчёта
  • Вольный обзор языков программирования
  • Java — великий и могучий
  • Репортажная зеркальная цифровая фотокамера Nikon D500. Новый флагман для искушенных.
  • Учимся пилотировать квадрокоптер. Часть 2


  • А что вы думаете? Напишите в комментариях!
    Кликните на изображение чтобы обновить код, если он неразборчив



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

    Полная версия правил
журнал Генеральный директор


Что бывало...

Проверьте скорость вашего интернета!


Самое популярное
    
Наши друзья
Vivaldi

Студия 3D-печати PRO3D

Майки с картинками

Самоклейкин

Смарт

Hoster


Секреты машинного кода



В одной из прошлых статей мы поговорили о языках программирования высокого уровня. Сегодня мы опустимся гораздо ниже — в самые недра программных систем и попробуем разобраться в том, на каком «языке» говорит с пользователями и программистами сама ЭВМ.


Двоичный код


Мы часто вкладываем в это понятие самые различные смыслы. «Двоичный код», «машинный код», «программный код». Между тем, значение этих терминов различно. А вот использование в машинной математике двоичного кода очевидно и вполне логично.

Цифровая электроника работает с двумя типами электрических сигналов, или, если точнее, с двумя «состояниями» — высоким уровнем сигнала  в его источнике либо передатчике и низким его уровнем. Высокий уровень сигнала — это «ноль», низкий — «единица». Это наиболее надежная схема передачи сигналов, которая дает стабильный результат. Какие бы помехи не возникали на пути сигнала, мы всегда поймем, где «единица», а где — «ноль».


На такую схему великолепно ложится двоичная система счисления. Справедливости ради стоит отметить, что впервые такая система была описана в древнекитайской «книге Перемен». В дальнейшем двоичная система счисления только обретала теоретическую основу. Математик Лейбниц, например, доказал, что основой системы счисления может быть любое число, кроме нуля. Были, а, возможно и будут и другие теоретические работы в этом направлении.  Сейчас используются, как минимум, три системы счисления — двоичная, восьмеричная, шестнадцатиричная. Но нас интересует именно двоичная.


Как работает «двоичный код»


С помощью этого кода можно легко перевести десятичное число в понятное компьютеру двоичное. Возьмем число (например, 11001). Пронумеруем его с конца, причем, нумерацию начнем с нуля. Порядковый номер каждой цифры станет степенью, в которую мы возведем основание системы счисления (2), затем перемножим результат с цифрой. Нужно провести эту операцию с каждой цифрой двоичного числа, затем суммировать результат:


1*(2^4)+1*(2^3)+10*(2^2)+0*(2^1)+1*(2^0)
16+8+0+0+2 = 25

Теперь попробуем перевести число 25 обратно, в двоичный код. Используем для этого простейшую арифметическую операцию — получение остатка от деления.


25/2 =  1
12/2 =  0
6/2   =  0
3/2   =  1
1/2   =  1


В результатах, снизу вверх, читаем наше число.

Таким образом считать довольно легко, причем, даже на «пальцах». У нас ведь на руке пять пальцев? Если каждому присвоить единицу или ноль (ноль – палец согнут, единица – выпрямлен) и прикинуть значение основания системы, возведенной в нужную степень, то можно считать от нуля и до 1023. Правда, в случае с числом 25 жест руки будет немного неприличным.

Для любителей посчитать есть сегодня любые инструменты — начиная от калькулятора и заканчивая таблицами разрядности.

С дробными числами расчет ведется по иному. Всем цифрам до «запятой», степень присваивается в обратном порядке, от нуля, как и обычно, а вот числа после «запятой» сопровождаются отрицательной степенью, причем, она уменьшается от запятой. Вот как это выглядит:


Число: 

1101,101
1*(2^3)+1*(2^2)+0*(2^1)+1*(2^0)+1*(2^-1)+0*(2^-2)+1*(2^-3)
8+4+0+1 + 0,5+0+0,125 = 13,625

В остальном, операция выполняется также.


Как считает компьютер?


Компьютер не может, допустим, приписать «минус» к двоичному числу и считать его отрицательным. Этот минус нужно где-то поместить. Простейший способ — снабдить число дополнительным знаком (в восьмиразрядной ячейке, в этом случае мы сможем поместить семиразрядное двоичное число, где дополнительный разряд будет означать знак — единица отрицательный, ноль — положительный). Это называется «прямым кодом».


0111010
0111010

Но отрицательные числа лучше представлять по-другому — для того, чтобы компьютер мог с ними работать. «Единица» в крайнем разряде по-прежнему будет выполнять роль «маркера» отрицательного числа. Но остальные разряды требуется инвертировать (то есть, разряды с нулями заменить единицей и наоборот). На месте остается только маркер.


1000101

Теперь к числу прибавляем единицу:


1  1000110

Для чего же вся эта малопонятная морока? – спросит читатель. Для того, чтобы ЭВМ смогла выполнять операции с числами.


0 0111010+0 0111010 = 1110100   (58+58 = 116)

Знаковые разряды в этом случае просто не учитываются.

Но если число отрицательное — все меняется. Переводим его в дополнительный или обратный код (инвертируем все разряды кроме знакового, затем прибавляем единицу) и складываем снова, затем из результата вычитаем единицу и инвертируем его:

1  1000110+1 1000110  = 1 0001011 = 1 1110100 (-116)

Вычитание работает по тому же принципу, умножение — путем последовательного сложения (я очень сильно упрощаю но принцип именно таков). Что касается операции деления, то она реализуется путем неоднократного сложения делимого с дополнительным кодом делителя до получения необходимого результата.

Подробно эти действия  описывать не буду. Расскажу лишь про арифметический сдвиг. Это ещё одна «хитрость», которая позволяет упростить вычисления. Если сдвинуть двоичное число вправо или влево, то оно меняется, согласно определенным правилам.


1111101 = 125

Сдвиг вправо (слева добавляем единицу, последний разряд убираем):


1111110 = 126

Ещё один сдвиг вправо:


1111111 = 127

С дополнительным кодом — ещё интереснее. Сдвиг числа влево дает умножение на 2, вправо – деление на 2. Это очень удобно для умножения и деления целых чисел на числа, равные степени 2 (2, 4, 8, 16, 32, 64). Существует множество вариантов сдвига — арифметический (мы его выполнили), циклический, логический и т.д. Каждый выполняет свою задачу.

Добавлю, что всё, о чем я говорил — это «азы» компьютерных вычислений. Когда-то они выполнялись компьютером именно так. Сегодня они ведутся уже на ином уровне. Персональный компьютер выполняет расчеты при помощи встроенного в ядро основного ЦПУ математического сопроцессора, где каждая операция выполняется одной командой, что значительно ускоряет работу. Математический сопроцессор оптимизирован именно для вычислений, эти функции заложены в него на аппаратном уровне. Но описание работы сопроцессора — это тема для отдельной статьи или даже целой книги.


Кодируем… двоичный код


Сейчас мы подошли к самому главному моменту: мы уже знаем, что компьютер выполняет операции с числами в двоичном коде. Причем, это единственный пока вариант для ЭВМ и не такой уж сложный, если вдуматься, для нашего с вами понимания. Остается выяснить, как же объяснить компьютеру, что он должен делать с числом (числами), чтобы получить результат.

Для этого, очевидно, нужны команды, которые будут точно поняты и интерпретированы центральным процессором. А поскольку ничего, кроме двоичных чисел ЭВМ не понимает, то команды должны состоять из определенного набора цифр — машинной инструкции. Она представляет собой запись, состоящую из нескольких элементов — начального и конечного маркеров и самого её «тела».


Машинные инструкции разрабатываются для каждого семейства процессоров и некоторых отдельных их разновидностей. Поддержка инструкций заложена в самом процессоре.

Конечно, программу из таких инструкций написать очень и очень трудно, так как ручное управление процессором — задача нетривиальная и трудоемкая, для этого существуют специальные языки нижнего уровня, которые работают напрямую с процессором — ассемблеры. Специальная программа компилятор, превращает команды ассемблеров в машинные инструкции, которые располагаются в специальных бинарных (двоичных) файлах.

Коды, коды, коды

В принципе, кодирование информации происходит в компьютере неоднократно. Это циклический процесс, реализованный и на программном и на аппаратном уровнях. Начинается он со скан-кода (в IBM-совместимых компьютерах), с помощью которого драйвер клавиатуры распознает нажатия и отпускания клавиш.

Далее в электронных документах, с которыми мы работаем, символы кодируются при помощи кодовых таблиц (кодировок) — (КОИ - 8, СР1251, СР866, Мас, ISO, Unicode), в которых на один символ отводится от одного и более байт. Коды у одинаковых символов в разных кодировках различны. Допустим, двоичное число 11000010 в кодировке KOI-8 будет означать строчную букву «б», а в кодировке CP1251 — прописную «В».

С помощью стандартных кодировок записываются и команды абсолютно всех языков программирования. А затем программа проходит через транслятор, который снова кодирует её, только уже в понятные процессору машинные инструкции либо в ассемблер. В принципе, транслятор решает массу задач но это — тема для другого разговора. Наша цель — составить более-менее стройную картину преобразования информации внутри ЭВМ.

Ассемблеры

Собственно, мы уже несколько раз упоминали это «семейство» низкоуровневых языков программирования. Сейчас их используют реже, чем это было раньше, так как появилось немало более удобных, высокоуровневых языков. Тем не менее, ассемблеры всё же применяются — когда нужен небольшой но высокоэффективный фрагмент программного кода. Ассемблерные вставки поддерживаются и в ряде высокоуровневых языков программирования, с целью ускорения работы кода.

Ассемблеры — аппаратно-ориентированные языки, они привязаны к архитектуре процессора, поэтому, их синтаксис для разных процессоров будет различен. Но их объединяет один принцип. Команды процессора, которые представляют собой просто числа, в ассемблерах представлены как «мнемонические» символы. Чтобы не расшифровывать это понятие, я просто продемонстрирую несколько небольших программ.

Вот как будет выглядеть программа на ассемблере 8086 (x86-совместимая), которая выводит на консоль Hello World:

use16                     
org 100h                
 
    mov dx,hello      
    mov ah,9                    
    int 21h               
 
    mov ax,4C00h   
    int 21h              
hello db 'Hello, world!$'

Понять, что здесь написано не так уж и трудно. Use16 – генерация 16-битного кода, регистр DX (mov dx) содержит адрес строки (строка заканчивается значком $ и объявляется директивой db), регистр AH (mov ah) содержит 9 - номер функции DOS, int 21h — обращение к функциям DOS. 4C00h — завершение программы, где 0 — успешное её завершение.
Может даже показаться, что программирование на ассемблере — легкая задача. Но это отнюдь не так. Если вы начнете делать что-то более или менее сложное, писанины будет куда больше. Вот, взгляните на вывод латинских букв по алфавиту в цикле:  
     
use16                
org 100h             
 
    mov ah,02h      
    mov dl,'A'       
    mov cx,26        
tt:
    int 21h         
    inc dl        
    loop tt   
 
    mov ah,09h      
    mov dx,press     
    int 21h        
 
    mov ah,08h   
    int 21h        
 
    mov ax,4C00h     
    int 21h          
press:
    db 13,10,'Press any key...$'

Кроме уже знакомых нам команд здесь есть и другие. 02h — вывод символа, dl — первый символ, Inc dl – следующий символ, 08h — ввод без отображения (чтобы не закрылась программа), 13,10 — возврат каретки, перевод строки, сх — счетчик повторений. Loop — метка, которая «выбрасывает» цикл к началу.Собственно, далеко не каждый, даже опытный программист, сможет работать с ассемблерами. Для этого нужно очень хорошо знать архитектуру процессора. Для промышленного программирования гораздо удобнее пользоваться языками высокого уровня, о которых мы говорили в прошлый раз.

Уровни общения

Команды ассемблера — это, практически, прямые инструкции для процессора, которые выполняются и трактуются однозначно. Машинный код программ, написанных на современных языках программирования высокого уровня, генерируется и оптимизируется автоматически, порой проходя несколько уровней кодирования. Это сделано для того, чтобы ускорить и упростить работу программиста. Но «упрощение», в данном случае — весьма относительное понятие.

Современные языки программирования представляют собой сложнейшие системы, которые позволяют относительно быстро создавать и тестировать программные комплексы любого масштаба и сложности.



Подписывайтесь и читайте новости от ITквариат раньше остальных в нашем Telegram-канале !

Поделитесь этой новостью с друзьями!

Эдуард ТРОШИН

Компьютерная газета

Заметили ошибку? Выделите ее мышкой и нажмите Ctrl+Enter!  

И еще на эту тему...
  • LG G6: большой тест - обзор
  • 11 секретов профессиональных фотографов из National Geogaphic
  • МЭСМ – наша точка отсчёта
  • Вольный обзор языков программирования
  • Java — великий и могучий
  • Репортажная зеркальная цифровая фотокамера Nikon D500. Новый флагман для искушенных.
  • Учимся пилотировать квадрокоптер. Часть 2


  • А что вы думаете? Напишите в комментариях!
    Кликните на изображение чтобы обновить код, если он неразборчив



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

    Полная версия правил
    ITквариат Powered by © 1996-2019