UProLa

Неокріпші думки

Пиш. св. ОС. Ч.3 – ПсЧМ part 2 (обер.! довг. пост)

with 2 comments

(У попередній серії)

Уважний читач мабуть одразу повинен був задати питання: “Ну добре, як працює ІНІЦІАЛІЗУВАТИ ТЕЛЕСКОП я ще зрозумів. Проте як Чак Мур вирішив проблему з 2+2=?”. І уважний читач має рацію, адже інтерпретатор без можливості оперування числами Чарльзу не був потрібний.

Числа і телескоп

В першу чергу Чак писав програму для людей з обсерваторії, для зручного керування дорогими пристроями. Тому він свідомо хотів наблизити синтаксис свого командного рядка до синтаксису людської мови. В якості прикладу він взяв фразу:

Повернути телескоп на 20 градусів проти год. стрілки

З першого погляду, інтерпретувати дану фразу на асемблері не можливо. Проте Чак глянув вдруге і побачив таке:

20 градусів проти-год-стрілки ПОВЕРНУТИ телескоп

Виділене капсом слово – дієслово, дія, певна функція, яка приймає на вхід параметри: що саме повернути та на який саме кут повернути. Як пам’ятає читач, у інтерпретаторі уже реалізовано слово TOKEN, котре зможе витягнути параметр “ЩО ПОВЕРНУТИ”. Проте як витягнути параметр “НА ЯКИЙ КУТ ПОВЕРНУТИ”? Очевидно, повинен бути спосіб передачі аргумента функції.

Числа і функції

Сучасні програмісти передають параметри в функції у дужках, наприклад так:

ITnet2.Server.SessionHost.Log.ItLogOffice("Message to log", false);

Іноді передаються і без дужок, наприклад так (у FoxPro я так роблю)

do sted2case with m.htmlbrow,0,,,,.F.,[/GETEDIT:TEXT]

І як правило думають, що це єдино можливий і єдино правильний спосіб передачі аргументів – писати аргументи після імені функції і сподіватись, що компілер сам все розрулить. Проте мені це нагадує анекдот про блондинку, яка вважає що хліб росте в магазинах. Так ось, відкрию таким програмістам секрет, як передаються аргументи на найнижчому рівні, на рівні асемблера: вони передаються ПЕРЕД викликом функції. Як це відбувається?

push "Message"   ; закинути аргументи в стек в звичайному порядку
push false       ;
call ITnet2.Server.SessionHost.Log.ItLogOffice
; функція сама почистила стек

або

push false       ; закинути аргументи в зворотньому порядку
push "Message"   ;
call ITnet2.Server.SessionHost.Log.ItLogOffice
add esp, length(false) + length("Message")    ; очистити стек після виклику функції, 
                                              ; вона сама не очищує

Перший спосіб передачі параметрів називається PASCAL, другий називається С (здогадайтесь, чому). Як видно, всі параметри передаються через стек, саме тому Чак вирішив у своєму інтерпретаторі передавати параметри через стек (якщо чесно, існує ще два способи передачі параметрів: через регістри і через пам’ять. Через регістри погано передавати, оскільки там немає ніякої уніфікації + можна запортити ненароком програму, а через пам’ять передавати взагалі не зручно, уже простіше передати через стек адресу на цю пам’ять і хай функція сама розбереться що до чого).

Числа

Як вам відомо, процесор оперує числами, а всякі об’єкти, рядки, потоки, типи – це всього лиш абстракції над числами. Чак слідував мінімалізму, тому одразу відмовився від будь-якої типізації, яка б занадто ускладнила роботу його програми. Числа і все. Проте дещо відмінне від роботи процесора він придумав. Він вирішив розділити стек процесора і стек чисел. Навіщо? В стек процесора за допомогою “call” i “push” дуже часто кладеться “допоміжна” інформація, яка заважає нормально працювати з числами-даними. Власне задля зручності роботи і було зроблене це нововведення, яке згодом лягло в основу ідеології “стекової нотації”.

Ідея проста:

  1. Читаємо наступне слово з вхідного рядка
  2. Шукаємо слово в словнику. Якщо знаходимо, то виконуємо його і пересуваємось на пункт 1
  3. Якщо слово не знайдено в словнику, то пробуємо прочитати його як число. Якщо вдалося, то кладемо це розпарсене число на вершину стека даних і пересуваємось на пункт 1
  4. Якщо слово не знайдено в словнику і не є числом, то видаємо помилку, типу “я хз шо за хню ти ввів”

В результаті маємо: якщо потрібно передати якійсь функції аргумент, ми спершу кладемо цей аргумент на стек, а потім викликаємо функцію (яка візьме верхнє значення зі стеку та обробить). Виглядати це буде приблизно так:

> 1 ВИВЕСТИ-ЧИСЛО-НА-ЕКРАН 2 ВИВЕСТИ-ЧИСЛО-НА-ЕКРАН 3 ВИВЕСТИ-ЧИСЛО-НА-ЕКРАН
123

А ось інший приклад:

> 1 2 3 ВИВЕСТИ-ЧИСЛО-НА-ЕКРАН ВИВЕСТИ-ЧИСЛО-НА-ЕКРАН ВИВЕСТИ-ЧИСЛО-НА-ЕКРАН
321

Оскільки “ВИВЕСТИ-ЧИСЛО-НА-ЕКРАН” забирає зі стеку верхній елемент після першого “ВИВЕСТИ-ЧИСЛО-НА-ЕКРАН” на стеку залишається 2 елементи, після другого – уже один елемент, а після третього – стек уже порожній. Власне, саме це і потрібно реалізувати.

Ти так і не відповів на питання

З такого боку проблема “2+2=?” набуває іншого вигляду:

> 2 2 + ВИВЕСТИ-ЧИСЛО-НА-ЕКРАН
4

На стек кладется 2 числа, потім викликається функція “+”, яка забирає два верхні числа зі стеку і кладе замість них іхню суму а вже потім ця сума (верхнє число на стеку) виводиться на екран. Все просто.
Вернемось до телескопів

20 градусів проти-год-стрілки ПОВЕРНУТИ телескоп

Тепер це можна зрозуміти так: на стек кладеться число 20, функція “градусів” перетворює верхнє число на стеку у кількість градусів (тобто, нічого не робить), слово “проти-год-стрілки” множить верхнє число на стеку на коефіцієнт “-1” (ну, щоб в іншу сторону), слово “ПОВЕРНУТИ” бере верхнє число зі стеку (тепер уже -20), вибирає із вхідного потоку слово “телескоп” і робить свою справу, адже тепер відомі всі аргументи! В випадку потреби можна також написати:

1 радіан за-год-стрілкою ПОВЕРНУТИ купол

Слово “радіан” перетворить кількість радіан у вершині стеку у кількість градусів і покладе на вершину, слово “за-год-стрілкою” нічого не буде робити і слово “ПОВЕРНУТИ” отримає на вхід кут у 57 градусів та об’єкт “купол”. Як бачите, стекова робота з аргументами досить зручна, недарма китайці використовують її у своїй мові.

Реалізація

PrintAX

PrintAX:
       pusha
       movzx bx, byte[BASE]
       mov cx, 0
       mov dx, 0                  ; обнуляємо, оскільки ділене виду DX:AX
           .div_loop:
              div bx
              cmp dl, 9           ; якщо частка більша дев'яти,
                jle .cyfer_system ;    то це цифра,
              add dl, 7           ;    інакше - перетворюємо у букву
             .cyfer_system:       ;
              add dx, 0x0E30      ; 0..9 і 17..35  ->  '0'..'9' і 'A'..'Z'
                                  ; 0E - функція виводу символу (для оптим.)
              push dx             ; зберігаємо ASCII цифру у стек
              inc cl              ; кількість циферок++
              mov dx, 0           ; лишнє тепер
              cmp ax, 0           ; якщо AX != 0,
           jnz .div_loop          ;   то продовжуємо ділити
           .print_loop:           ;   інакше - виводимо байти з стеку,
              pop ax              ;   в CL їхня кількість
              int 0x10
           loop .print_loop
       popa
       ret

Просто виводить число в AX на екран. Дуже потрібна функція.

ParseNumber

; SI - word to parse
; AX - result
ParseNumber:
        push si
        push dx
        push cx
        movzx cx, byte[si]
        inc si
        mov ax, 0
        mov dx, 0
            .lloop:
                lodsb
                sub al, '0'
                cmp al, 9
                  ja .error
                imul dx, 10
                add dx, ax
                  jc .error
            loop .lloop
            jmp .end
      .error:
        pop cx
        pop dx
        pop si
        stc
        ret
      .end:
        pop cx
        mov ax, dx
        pop dx
        pop si
        clc
        ret

Зворотня оперція, перетворити ASCII слово у беззнакове число і записати в AX. У випадку помилки встановлює прапорець CF (Carry Flag) у одиничку, у випадку успіху – нулик. Ніяких перевірок на переповнення не виконується, тобто числа більші 65535 будуть розпарсені у щось рандомне. Мало того, сприймає числа тільки у десятковій системі. Як бачите, не так вже й елементарно перетворити рядок у число (особисто я звик уже до int.Parse(“65535”))

GetToken

Нова версія GetToken:

; al - відділювач слова
GetToken:
       ; Алгоритм такий:
       ; 1. Якщо рядок кінчився, то виходимо
       ; 2. Зчитати прогалики. Якщо рядок кінчився, то виходимо
       ; 3. Вирахувати довжину послідовності буквів
       ; 4. Переписати букви з вхідного рядка в токен
       mov byte[Token], 0  ; обнулити слово
       mov ah, al
       mov al, 32           ; прогалик, роділювач
       cmp cl, 0           ; якщо рядок уже закінчився то виходимо
         jz .end

       repe scasb           ; проітерувати по прогаликам
         jz .end           ; REPE працює, доки у нас в рядку прогалики, тобто, доки
                         ; ZF=1. Якщо ми вийшли з REPE і у нас досі ZF=1, то це значить тільки
                         ; одне - кінчився рядок, CX=0. Ну, якщо в кінці рядка одні прогалики,
                         ; то їх можна ігнорувати і виходити з процедури

       inc cl                  ; Якщо ж ZF=0, значить ми наткнулись на букву. Проте, хоч ми й наткнулись,
       dec di                  ; проте зчитали її. Відновити позицію на зчитану букву
       mov si, di           ; зберегти DI
       mov al, ah           ; обмежувач слова (не обов'язково прогалик)

       repne scasb           ; ітеруємо по буквам
         jnz .notfnd           ; див міркування вище, майже аналогічно
       dec di
       inc cl
     .notfnd:
       push cx
       mov cx, di           ;

       sub cx, si           ;CX = DI - SI, а SI - збережений DI, вказівник на першу букву
       mov byte[Token], cl ; скільки буквів у слові
       mov di, Token + 1   ; куди писати слово
       rep movsb
       mov di, si           ; відновлюємо "справделивість"

       pop cx
       repne scasb           ; зчитати розділювач
     .end:
       ret

Основна відмінність від старого варіанту – повністю нова реалізація ))) і можливість задавати символу-обмежувача слова. Як правило, обмежувачем буде прогалик, проте деякі слова будуть обмежуватись іншим символом. Навіщо це – взнаєте пізніше.

PushAX, PopAX

; стек даних
SPpointer dw 0             ; збережене значення вказівника SP перед черговим словом.  
                           ;   У випадку якоїсь помилки вказівник стеку повернеться 
                           ;   в цю точку.                           
StackPointer dw StackBase  ; власне, вказівник SP, адреса на настпуну вільну комірку  
                           ;  пам'яті. Спочатку ставимо вказівник на адресу StackBase.
                           ;  Стек буде рости в сторону більших адрес
StackBase db 0             ; основа стеку
rb 50                      ; резервуємо місце на 25 елементів для стеку даних

PushAX:
    pusha
    mov bx, [StackPointer]
    mov [bx], ax
    add [StackPointer], 2
    popa
    ret

PopAX:
    sub [StackPointer], 2
    cmp [StackPointer], StackBase   ; якщо ми дійшли до дна стеку, то вивести помилку
      jl .stackerror
    mov bx, [StackPointer]
    mov ax, [bx]
    ret
     .stackerror:
    mov [StackPointer], StackBase
    mov sp, [SPpointer]
    call SimplePutEnd
    mov si, .error_message
    call PutPASCAL
        pop ax          ; цікавий хак. В даному місці число на вершині стеку
                        ;  процесора вказує на наступну команду в коді ЯДРА. Ми
        push main       ;  просто підмінюємо це число на нову адресу - адресу ядра (main). 
        ret             ; відповідно тут, коли ми виходимо з функції, ми 
                        ;  відновлюємось прямо в точку main і починаємо знову
                        ;  зчитувати віхдний рядок користувача!
    .error_message db 16,"Stack is empty",13,10

Очевидно, PushAX закидує у стек даних число в AX, а PopAX – забирає в AX число на вершині стеку. Реалізовані ці функції у мене, звісно, дуже криво, проте працюють і мені цього поки-що достатньо.
І ще, зауважте, що в стек кладеться 16 бітне число і всюди я оперую саме 16-бітними числами, по 2 байти.

Ядро

По традиції, код ядра приводити не буду, щоб при великому бажанні читач сам мав можливість відтворити цей історичний шлях.

Слова

Для того, щоб відчути всю потужність даного механізму нам знадобиться кілька слів. Тут я буду приводити слова, які уже стандартні в Форті, тому не дивуйтесь їм – така уже історія.

; ВИВЕСТИ-ЧИСЛО-НА-ЕКРАН. Дана функція викликається дуууже часто, тому і слово повинно 
; бути якомога коротшим. 
PRINTNUM:
        dw SPACE
        db 1,'.'
           call PopAX
           call PrintAX
           ret
; Вивести символ на вершині стеку на екран. 
EMIT:
        dw PRINTNUM
        db 4, 'emit'
           call PopAX
           call SimplePutChar
           ret
; Перевід на новий рядок
CR:
        dw EMIT
        db 2, 'cr'
           call SimplePutEnd
           ret
; Вивести рядок. За рахунок того, що в GetToken можна задати символ-обмежувач,
;  ми і виводимо рядок, обмежений символом "
STRING_PRINT:
        dw CR
        db 2, '."'
           mov al, '"'
           call GetToken
           pusha
           mov si, Token
           call PutPASCAL
           popa
           ret
; +		   
PLUS:
        dw STRING_PRINT
        db 1, '+'
           push dx
           call PopAX
           mov dx, ax
           call PopAX
           add ax, dx
           call PushAX
           pop dx
           ret
; -
MINUS:
        dw PLUS
        db 1, '-'
           push bx
           call PopAX
           mov bx, ax
           call PopAX
           sub ax, bx
           call PushAX
           pop bx
           ret
; дублює верхній елемент на стеку
DUPP:
        dw MINUS
        db 3, 'dup'
           call PopAX
           call PushAX
           call PushAX
           ret
; викидує зі стеку верхній елемент
DROP:
        dw DUPP
        db 4, 'drop'
           call PopAX
           ret
; Аналогічно STRING_PRINT ми зчитуємо рядок, обмежений символом ')' і просто ігноруємо. 
; Нехай вас не дивує, що дужки використовуються для коментування, насправді це логічно ))
COMMENT:
        dw DROP
        db 1, '('
           mov al, ')'
           call GetToken
           ret   

Розбір польотів

  1. Закидуємо в стек 3 числа і виводимо їх. Виводяться вони у зворотньому порядку, така ось ососбливість стекової організації пам’яті.
  2. Виводимо Хеллоу вордл!!! у два рядки. Як видно, коментарі чудсно ігноруються інтерпретатором
  3. Перевіряємо спроможність до математики
  4. Закидуємо на стек 3 числа, сумуємо два верхніх (на стеку уже два числа), віднімаємо два верхніх (на стеку 1 число), знову сумуємо два верхніх числа. Оскільки в стеку всього лиш 1 елемент, то звісно слово “+” обламується і виводиться помилка – “Stack overflow” “Stack is empty”
  5. Показано, як можна обійтись без слова CR, якщо знати, що символ 10 (Line Feed) переводить курсор на рядок нижче, а 13 (Carriage Return) повертає курсор в початок рядка.(
  6. Комплексний прилад, який показує, яким способом можна зробити тривіальну річ – вивести “01234”. Використано знання про те, що в ASCII символ “0” стоїть під номеров 48 (або 0х30). Хоч цей метод і надуманий, проте тут добре видно, що якби у нас були циклічні структури, то реалізувати можна було би БУДЬ-ЯКИЙ алгоритм!
  7. Уважний читач помітив би, що в попередньому прикладі на стеку лишилось одне число (52, до-речі), тому після виконання DROP воно викидується зі стеку і не виводить помилки.
  8. А тут продемонстровано приклад помилки в системі =). Довжина слова, яку ми встановили, дорівнює 32 символам. Проте коли ми пробуємо зчитати довгий коментар у слово, він не влазить повністю і псує код, який знаходиться опісля визначення змінної Token. Псує так сильно, що система висне. І таке буває, коли не робити ніяких перевірок…

Інтерпретатор у 512 байт

До цих пір моя програма ще влазить у бут-сектор розміром 512 байт. Тоді як деякі пихають у ці 512 байт загрузчик першого рівня, який завантажує загрузчик 2 рівня, який уже завантажує операційну систему, я у ці пів-Кб впихнув цілу ОС! Звісно, я жартую, ніяка у мене це ще не ОС, проте це натяк – світ котиться в сферу потужних процесорів та терабайт пам’яті, тоді як для щастя потрібно зовсім небагато.

Розширення системи

Разом з тим ці 512 байт сильно мене обмежили. Більше жодного слова додати не вдалось. Тому я і вирішив написати загрузчик першого рівня, який уже завантажить мою ОС. Це виявилось нескладно:

;-------------------------------------------------
; MBR Loader level 1
;-------------------------------------------------
ORG 7C00h
mov ax, 0x0200 + 128
mov bx, main
mov cx, 2
mov dx, 0
int 13h
jmp main
times (512-2-($-7C00h)) db 0
db 055H, 0AAH

;++++++++++++++++++++
;--   MAIN    -------
;++++++++++++++++++++
main:
;
;  .... тут має бути купа коду ....
;

;=================================================
; Rest 128 sectors
;=================================================
times (512*129-($-7C00h)) db 0

“int 13h” – це робота з диском. Весь код спрямований на одне – завантажити з диску в оперативну пам’ять 128 секторів (по 512 байт), що складе в сумі 64 Кб (межу пам’яті в MS-DOS, чисто для символізму). Тому, тепер у нас є на все про все не півкілобайта, а цілих 64! Ох тут можна й розгорнутись!

Принципи Форту

Перед тим, як почати показувати вам нові слова, розповім кілька принципів Форт-інтерпретаторів, щоб було більш зрозуміло.

  • Те, що я уже зробив, це ще не Форт у буквальному значенні цього слова, проте це Форт у значенні ідеології. Максимальна зручність при мінімально можливому розмірі, макро-надбудова над асемблером з можливостями мов високого рівня, наявність діалогового режиму, абстрагованість від системи (у прикладах я показував як можна замінити виклик CR двома EMIT-ами, проте тому і існує слово CR, щоб абстрагуватись від способу переведення на новий рядок у різних комп’ютерах), словник та слова – ось що таке Форт-ідеологія, ось що є суттю. Форт – це не мова програмування, це спосіб мислення.
  • Як видно з попереднього твердження, СТЕК ДАНИХ не є необхідним, щоб мова називалась Форт-подібною. В історії існують приклади Форт-систем БЕЗ стеку даних! Просто, як правило стек реалізується через те, що він є найбільш зручним при найменших затратах на його організацію.
  • Історично так склалось, що Форт-слова повинні писатись КАПСОМ, проте це зовсім не означає, що я не можу писати слова маленькими буквами. Від цього мій Форт не перестане бути Фортом.
  • У Форті дуже мало внутрішніх перевірок на коректність роботи програми та коректність дій користувача, а ті які є можна завжди можна обійти. Це наслідок того, що Форт – всього лиш надбудова над асемблером і як в асемблері, так і в Форті дозволено робити все, що заманеться. Мабуть через це Форт досі вважається “метрвою”, нежиттєздатною мовою. В світі великих технологій, швидкого та розприділеного між багатьма людьми програмування, в світі мульйонів вірусів Форт – як біла ворона, ба, навіть як незаймана дівчина в товпі агресивних садистів…
  • Хоча насправді, доки живуть хакери, доти буде жити й Форт! GRUB L.2, OpenBIOS, PostScript – приклади того, що Форт живіший всіх живих і ніколи не помре як ідея
  • Форт-ідеологію не можливо збагнути, доки сам не напишеш хоча б частину свого власного Форт-інтерпретатора
  • Дуже часто у Форті використовуються конструкції, які вганяють у ступор звичайних програмістів. По-перше, стекове мислення, по-друге, – коментарії в дужках (ососбисто мене це вбивало спочатку, проте тепер я розумію, що сам пишу коментар до фрази і він знаходить в дужках, а отже коментарі в дужках – це так … природньо…), по-третє, – використання символів як ключових слів. Ось неповний список стандартних односимвольних слів:
    • . (крапка) – вивести число в вершині стеку на екран
    • ( (дужка) – означає початок коментаря
    • @ (собачка) – зчитати число в пам’яті
    • ! (знак оклику) – записати число в пам’ять
    • # (шарпик) – забрати у числа останню цифру
    • [ (кв. дужка) – перейти у режим інтерпретації
    • ‘ (апостроф) – знайти слово в словнику
    • \ (зворотній слеш) – однорядковий коментар
    • : (двокрапка) – створити заголовок для слова і перейти у режим компіляції
    • , (кома) – записати число зі стека по адресі HERE і пересунути вказівник DP на CELL байт вперед
  • А які можна комбінації між цими функціями робити!… – це справді виглядає магією для людини, яка тільки розбирається у Форті, не кажучи вже про непосвячених. (хоча непевний, що стандартний Форт більш обфусковний ніж Perl)
  • Форт також має стандарт, ба, навіть цілих три штуки – Форт-79, Форт-84, Форт-94 (по рокам, у яких стандарт був прийнятий). Стандарти пишуться на основі досвіду, тому я також буду намагатись слідувати стандарту. Проте, стандарту потрібно дотримуватись тоді, коли потрібна якась взаємодія між програмами, написаними різними людьми. Я пишу поки що сам, тому на деякі пункти стандарту вільно кладу. І від цього мій Форт не перестане бути Фортом.
  • Коли питаєш у людини, яка довго програмує на Форті, “які переваги у Форта перед іншими мовами?”, – майже завжди слідує відповідь такого типу: “Форт має такі переваги над іншими мовами: стек даних, можливість зміни роботи інтерпретатора на льоту, можливість створення нових конструкцій мови, мінімально можливий генерований код, відсутність обмежень, простота відладки програми, простота синтаксису і ще багато-багато інших переваг, типу реалізація інтерпретатора Форта на Форт займає всього 5 рядків, зручний інтерактивний режим,… (тут у мене уже фантазії не вистачає придумувати щось далі, я ж ще не супер-пупер форт-програмер)”
  • У Форті відсутня типізація, і це є принципом, а не недоліком. Якщо програмісту потрібен якийсь тип, будь-ласка, реалізовуй сам. Благо Форт дозволяє це робити.
  • Нехай вас не лякає, що арифметика Форта цілочисленна. При сильній потребі реалізувати дробові числа можливо і навіть кількома способами

Сподіваюсь, частина питань тепер відпаде, а отже, можна приступити до наступної частини саги – вивченню нових слів.

(Далі можливо буде)

Written by danbst

Серпень 30, 2010 at 21:06

Оприлюднено в Програмування

Відповідей: 2

Subscribe to comments with RSS.

  1. Да, жаль, що продовження немає, якось обірвано вийшло, причому обірвалось на найцікавішому — визначенні нових слів. Ти дороблював це?

    Я тут трохи зацікавився Фортом: у мене з’явилась підозра, що він може стати middle-level language для генерацїі із високорівневіших, але поки я не спробую його на смак, то нічого певного сказати не зможу. Але було б добре, якби можна було за допомогою Форта забутстрапити високорівневішу мову, він також міг би, здається, стати крос-платформним асемблером.

    dmytrish

    Березень 15, 2013 at 15:55

    • Дороблював хіба-що у межах диплому, на AVR. А так – інтерес пропав.

      А форт і так є кросплатформовим асемблером. Здається, в OpenBIOS юзають форт. З фортом багато проблем, щоправда…

      danbst

      Березень 15, 2013 at 21:50


Залишити відповідь

Заповніть поля нижче або авторизуйтесь клікнувши по іконці

Лого WordPress.com

Ви коментуєте, використовуючи свій обліковий запис WordPress.com. Log Out / Змінити )

Twitter picture

Ви коментуєте, використовуючи свій обліковий запис Twitter. Log Out / Змінити )

Facebook photo

Ви коментуєте, використовуючи свій обліковий запис Facebook. Log Out / Змінити )

Google+ photo

Ви коментуєте, використовуючи свій обліковий запис Google+. Log Out / Змінити )

З’єднання з %s

%d блогерам подобається це: