mirror of
https://github.com/LCTT/TranslateProject.git
synced 2025-01-13 22:30:37 +08:00
Merge remote-tracking branch 'LCTT/master'
This commit is contained in:
commit
03fecee832
@ -1,30 +1,30 @@
|
||||
[#]: collector: (lujun9972)
|
||||
[#]: translator: (LazyWolfLin)
|
||||
[#]: reviewer: ( )
|
||||
[#]: publisher: ( )
|
||||
[#]: url: ( )
|
||||
[#]: reviewer: (wxy)
|
||||
[#]: publisher: (wxy)
|
||||
[#]: url: (https://linux.cn/article-10521-1.html)
|
||||
[#]: subject: (An Introduction to Go)
|
||||
[#]: via: (https://blog.jak-linux.org/2018/12/24/introduction-to-go/)
|
||||
[#]: author: (Julian Andres Klode https://blog.jak-linux.org/)
|
||||
|
||||
Go 简介
|
||||
Go 编程语言的简单介绍
|
||||
======
|
||||
|
||||
(以下内容是我的硕士论文的摘录,几乎整个 2.1 章节,向具有 CS 背景的人快速介绍 Go)
|
||||
(以下内容是我的硕士论文的摘录,几乎是整个 2.1 章节,向具有 CS 背景的人快速介绍 Go)
|
||||
|
||||
Go 是一门用于并发编程的命令式编程语言,它主要由创造者 Google 进行开发,最初主要由 Robert Griesemer、Rob Pike 和 Ken Thompson开发。这门语言的设计起始于 2017 年,并在 2019 年推出最初版本;而第一个稳定版本是 2012 年发布的 1.0 版本。
|
||||
Go 是一门用于并发编程的命令式编程语言,它主要由创造者 Google 进行开发,最初主要由 Robert Griesemer、Rob Pike 和 Ken Thompson 开发。这门语言的设计起始于 2007 年,并在 2009 年推出最初版本;而第一个稳定版本是 2012 年发布的 1.0 版本。[^1]
|
||||
|
||||
Go 有 C 风格的语法(没有预处理器),垃圾回收机制,而且类似它在贝尔实验室里被开发出来的前辈们:Newsqueak (Rob Pike)、Alef (Phil Winterbottom) 和 Inferno (Pike, Ritchie, et al.),使用所谓的 goroutines 和信道(一种基于 Hoare 的“通信顺序进程”理论的协程)提供内建的并发支持。
|
||||
Go 有 C 风格的语法(没有预处理器)、垃圾回收机制,而且类似它在贝尔实验室里被开发出来的前辈们:Newsqueak(Rob Pike)、Alef(Phil Winterbottom)和 Inferno(Pike、Ritchie 等人),使用所谓的 <ruby>Go 协程<rt>goroutines</rt></ruby>和<ruby>信道<rt>channels</rt></ruby>(一种基于 Hoare 的“通信顺序进程”理论的协程)提供内建的并发支持。[^2]
|
||||
|
||||
Go 程序以包的形式组织。包本质是一个包含 Go 文件的文件夹。包内的所有文件共享相同的命名空间,而包内的符号有两种可见性:以大写字母开头的符号对于其他包是可见,而其他符号则是该包私有的:
|
||||
|
||||
```
|
||||
func PublicFunction() {
|
||||
fmt.Println("Hello world")
|
||||
fmt.Println("Hello world")
|
||||
}
|
||||
|
||||
func privateFunction() {
|
||||
fmt.Println("Hello package")
|
||||
fmt.Println("Hello package")
|
||||
}
|
||||
```
|
||||
|
||||
@ -32,25 +32,16 @@ func privateFunction() {
|
||||
|
||||
Go 有一个相当简单的类型系统:没有子类型(但有类型转换),没有泛型,没有多态函数,只有一些基本的类型:
|
||||
|
||||
1. 基本类型:`int`、`int64`、`int8`、`uint`、`float32`、`float64` 等。
|
||||
|
||||
1. 基本类型:`int`、`int64`、`int8`、`uint`、`float32`、`float64` 等
|
||||
2. `struct`
|
||||
|
||||
3. `interface` \- 一组方法的集合
|
||||
|
||||
4. `map[K, V]` \- 一个从键类型到值类型的映射
|
||||
|
||||
5. `[number]Type` \- 一些 Type 类型的元素组成的数组
|
||||
|
||||
6. `[]Type` \- 某种类型的切片(指向具有长度和功能的数组)
|
||||
|
||||
7. `chan Type` \- 一个线程安全的队列
|
||||
|
||||
3. `interface`:一组方法的集合
|
||||
4. `map[K, V]`:一个从键类型到值类型的映射
|
||||
5. `[number]Type`:一些 Type 类型的元素组成的数组
|
||||
6. `[]Type`:某种类型的切片(具有长度和功能的数组的指针)
|
||||
7. `chan Type`:一个线程安全的队列
|
||||
8. 指针 `*T` 指向其他类型
|
||||
|
||||
9. 函数
|
||||
|
||||
10. 具名类型 - 可能具有关联方法的其他类型的别名(译者注:这里的别名并非指 Go 1.9 中的新特性“类型别名”):
|
||||
10. 具名类型:可能具有关联方法的其他类型的别名(LCTT 译注:这里的别名并非指 Go 1.9 中的新特性“类型别名”):
|
||||
|
||||
```
|
||||
type T struct { foo int }
|
||||
@ -58,9 +49,9 @@ Go 有一个相当简单的类型系统:没有子类型(但有类型转换
|
||||
type T OtherNamedType
|
||||
```
|
||||
|
||||
具名类型完全不同于他们的底层类型,所以你不能让他们互相赋值,但一些运输符,例如 `+`,能够处理同一底层数值类型的具名类型对象们(所以你可以在上面的示例中把两个 `T` 加起来)。
|
||||
具名类型完全不同于它们的底层类型,所以你不能让它们互相赋值,但一些操作符,例如 `+`,能够处理同一底层数值类型的具名类型对象们(所以你可以在上面的示例中把两个 `T` 加起来)。
|
||||
|
||||
Maps、slices 和 channels 是类似于引用的类型——他们实际上是包含指针的结构。包括数组(具有固定长度并可被拷贝)在内的其他类型则是值(拷贝)传递。
|
||||
映射、切片和信道是类似于引用的类型——它们实际上是包含指针的结构。包括数组(具有固定长度并可被拷贝)在内的其他类型则是值传递(拷贝)。
|
||||
|
||||
#### 类型转换
|
||||
|
||||
@ -93,66 +84,60 @@ const bar2 someType = typed // error: int 不能被赋值给 someType
|
||||
|
||||
### 接口和对象
|
||||
|
||||
正如上面所说的,接口是一组方法的集合。Go 本身不是一种面向对象的语言,但它支持将方法关联到命名类型上:当声明一个函数时,可以提供一个接收者。接收者是函数的一个额外参数,可以在函数之前传递并参与函数查找,就像这样:
|
||||
正如上面所说的,接口是一组方法的集合。Go 本身不是一种面向对象的语言,但它支持将方法关联到具名类型上:当声明一个函数时,可以提供一个接收者。接收者是函数的一个额外参数,可以在函数之前传递并参与函数查找,就像这样:
|
||||
|
||||
```
|
||||
type SomeType struct { ... }
|
||||
type SomeType struct { ... }
|
||||
|
||||
func (s *SomeType) MyMethod() {
|
||||
}
|
||||
|
||||
func main() {
|
||||
var s SomeType
|
||||
s.MyMethod()
|
||||
var s SomeType
|
||||
s.MyMethod()
|
||||
}
|
||||
```
|
||||
|
||||
如果对象实现了所有方法,那么它就实现了接口;例如,`*SomeType`(注意指针)实现了下面的接口 `MyMethoder`,因此 `*SomeType` 类型的值就能作为 `MyMethoder` 类型的值使用。最基本的接口类型是 `interface{}`,它是一个带空方法集的接口——任何对象都满足该接口。
|
||||
如果对象实现了所有方法,那么它就实现了接口;例如,`*SomeType`(注意指针)实现了下面的接口 `MyMethoder`,因此 `*SomeType` 类型的值就能作为 `MyMethoder` 类型的值使用。最基本的接口类型是 `interface{}`,它是一个带空方法集的接口 —— 任何对象都满足该接口。
|
||||
|
||||
```
|
||||
type MyMethoder interface {
|
||||
MyMethod()
|
||||
MyMethod()
|
||||
}
|
||||
```
|
||||
|
||||
合法的接收者类型是有些限制的;例如,命名类型可以是指针类型(例如,`type MyIntPointer *int`),但这种类型不是合法的接收者类型。
|
||||
合法的接收者类型是有些限制的;例如,具名类型可以是指针类型(例如,`type MyIntPointer *int`),但这种类型不是合法的接收者类型。
|
||||
|
||||
### 控制流
|
||||
|
||||
Go 提供了三个主要的控制了语句:`if`、`switch` 和 `for`。这些语句同其他 C 风格语言内的语句非常类似,但有一些不同:
|
||||
|
||||
* 条件语句没有括号,所以条件语句是 `if a == b {}` 而不是 `if (a == b) {}`。大括号是必须的。
|
||||
|
||||
* 所有的语句都可以有初始化,比如这个
|
||||
|
||||
`if result, err := someFunction(); err == nil { // use result }`
|
||||
|
||||
* `switch` 语句在 cases 里可以使用任何表达式
|
||||
|
||||
* `switch` 语句可以处理空的表达式(等于 true)
|
||||
|
||||
* 默认情况下,Go 不会从一个 case 进入下一个 case(不需要 `break`语句),在程序块的末尾使用 `fallthrough` 则会进入下一个 case。
|
||||
|
||||
* 所有的语句都可以有初始化,比如这个 `if result, err := someFunction(); err == nil { // use result }`
|
||||
* `switch` 语句在分支里可以使用任何表达式
|
||||
* `switch` 语句可以处理空的表达式(等于 `true`)
|
||||
* 默认情况下,Go 不会从一个分支进入下一个分支(不需要 `break` 语句),在程序块的末尾使用 `fallthrough` 则会进入下一个分支。
|
||||
* 循环语句 `for` 不仅能循环值域:`for key, val := range map { do something }`
|
||||
|
||||
### Goroutines
|
||||
### Go 协程
|
||||
|
||||
关键词 `go` 会产生一个新的 goroutine,一个可以并行执行的函数。它可以用于任何函数调用,甚至一个匿名函数:
|
||||
关键词 `go` 会产生一个新的 <ruby>Go 协程<rt>goroutine</rt></ruby>,这是一个可以并行执行的函数。它可以用于任何函数调用,甚至一个匿名函数:
|
||||
|
||||
```
|
||||
func main() {
|
||||
...
|
||||
go func() {
|
||||
...
|
||||
}()
|
||||
...
|
||||
go func() {
|
||||
...
|
||||
}()
|
||||
|
||||
go some_function(some_argument)
|
||||
go some_function(some_argument)
|
||||
}
|
||||
```
|
||||
|
||||
### 信道
|
||||
|
||||
Goroutines 通常和信道结合,用来提供一种通信顺序进程的扩展。信道是一个并发安全的队列,而且可以选择是否缓冲数据:
|
||||
Go 协程通常和<rub>信道<rt>channels</rt></ruby>结合,用来提供一种通信顺序进程的扩展。信道是一个并发安全的队列,而且可以选择是否缓冲数据:
|
||||
|
||||
```
|
||||
var unbuffered = make(chan int) // 直到数据被读取时完成数据块发送
|
||||
@ -170,21 +155,21 @@ otherChannel <- valueToSend
|
||||
|
||||
```
|
||||
select {
|
||||
case incoming := <- inboundChannel:
|
||||
// 一条新消息
|
||||
case outgoingChannel <- outgoing:
|
||||
// 可以发送消息
|
||||
case incoming := <- inboundChannel:
|
||||
// 一条新消息
|
||||
case outgoingChannel <- outgoing:
|
||||
// 可以发送消息
|
||||
}
|
||||
```
|
||||
|
||||
### `defer` 声明
|
||||
### defer 声明
|
||||
|
||||
Go 提供语句 `defer` 允许函数退出时调用执行预定的函数。它可以用于进行资源释放操作,例如:
|
||||
|
||||
```
|
||||
func myFunc(someFile io.ReadCloser) {
|
||||
defer someFile.close()
|
||||
/* 文件相关操作 */
|
||||
defer someFile.close()
|
||||
/* 文件相关操作 */
|
||||
}
|
||||
```
|
||||
|
||||
@ -199,7 +184,7 @@ func Read(p []byte) (n int, err error)
|
||||
|
||||
// 内建类型:
|
||||
type error interface {
|
||||
Error() string
|
||||
Error() string
|
||||
}
|
||||
```
|
||||
|
||||
@ -209,7 +194,7 @@ type error interface {
|
||||
n0, _ := Read(Buffer) // 忽略错误
|
||||
n, err := Read(buffer)
|
||||
if err != nil {
|
||||
return err
|
||||
return err
|
||||
}
|
||||
```
|
||||
|
||||
@ -217,21 +202,21 @@ if err != nil {
|
||||
|
||||
```
|
||||
func Function() (err error) {
|
||||
defer func() {
|
||||
s := recover()
|
||||
switch s := s.(type) { // type switch
|
||||
case error:
|
||||
err = s // s has type error now
|
||||
default:
|
||||
panic(s)
|
||||
defer func() {
|
||||
s := recover()
|
||||
switch s := s.(type) { // type switch
|
||||
case error:
|
||||
err = s // s has type error now
|
||||
default:
|
||||
panic(s)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 数组和切片
|
||||
|
||||
正如前边说的,数组是值类型而切片是指向数组的指针。切片可以由现有的数组切片产生,也可以使用 `make()` 创建切片,这会创建一个匿名数组以保存元素。
|
||||
正如前边说的,数组是值类型,而切片是指向数组的指针。切片可以由现有的数组切片产生,也可以使用 `make()` 创建切片,这会创建一个匿名数组以保存元素。
|
||||
|
||||
```
|
||||
slice1 := make([]int, 2, 5) // 分配 5 个元素,其中 2 个初始化为0
|
||||
@ -250,15 +235,19 @@ slice = append(slice, arrayOrSlice...)
|
||||
|
||||
切片也可以用于函数的变长参数。
|
||||
|
||||
### Maps
|
||||
### 映射
|
||||
|
||||
Maps 是简单的键值对储存容器并支持索引和分配。但它们不是线程安全的。
|
||||
<ruby>映射<rt>maps<rt></ruby>是简单的键值对储存容器,并支持索引和分配。但它们不是线程安全的。
|
||||
|
||||
```
|
||||
someValue := someMap[someKey]
|
||||
someValue, ok := someMap[someKey] // 如果键值不在 someMap 中,变量 ok 会赋值为 `false`
|
||||
someMap[someKey] = someValue
|
||||
```
|
||||
|
||||
[^1]: Frequently Asked Questions (FAQ) - The Go Programming Language https://golang.org/doc/faq#history [return]
|
||||
[^2]: HOARE, Charles Antony Richard. Communicating sequential processes. Communications of the ACM, 1978, 21. Jg., Nr. 8, S. 666-677. [return]
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
via: https://blog.jak-linux.org/2018/12/24/introduction-to-go/
|
||||
@ -266,7 +255,7 @@ via: https://blog.jak-linux.org/2018/12/24/introduction-to-go/
|
||||
作者:[Julian Andres Klode][a]
|
||||
选题:[lujun9972][b]
|
||||
译者:[LazyWolfLin](https://github.com/LazyWolfLin)
|
||||
校对:[校对者ID](https://github.com/校对者ID)
|
||||
校对:[wxy](https://github.com/wxy)
|
||||
|
||||
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
|
||||
|
@ -1,485 +0,0 @@
|
||||
[#]: collector: (lujun9972)
|
||||
[#]: translator: (qhwdw)
|
||||
[#]: reviewer: ( )
|
||||
[#]: publisher: ( )
|
||||
[#]: url: ( )
|
||||
[#]: subject: (Computer Laboratory – Raspberry Pi: Lesson 8 Screen03)
|
||||
[#]: via: (https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/screen03.html)
|
||||
[#]: author: (Alex Chadwick https://www.cl.cam.ac.uk)
|
||||
|
||||
Computer Laboratory – Raspberry Pi: Lesson 8 Screen03
|
||||
======
|
||||
|
||||
The Screen03 lesson builds on Screen02, by teaching how to draw text, and also a small feature on the command line arguments of the operating system. It is assumed you have the code for the [Lesson 7: Screen02][1] operating system as a basis.
|
||||
|
||||
### 1 String Theory
|
||||
|
||||
So, our task for this operating system is to draw text. We have several problems to address, the most pressing of which is probably about storing text. Unbelievably text has been one of the biggest flaws on computers to date. What should have been a straightforward type of data has brought down operating systems, crippled otherwise wonderful encryption, and caused many problems for users of different alphabets. Nevertheless, it is an incredibly important type of data, as it is an excellent link between the computer and the user. Text can be sufficiently structured that the operating system understands it, as well as sufficiently readable that humans can use it.
|
||||
|
||||
```
|
||||
Variable data types such as text require much more complex handling.
|
||||
```
|
||||
|
||||
So how exactly is text stored? Simply enough, we have some system by which we give each letter a unique number, and then store a sequence of such numbers. Sounds easy. The problem is that the number of numbers is not fixed. Some pieces of text are longer than others. With storing ordinary numbers, we have some fixed limit, e.g. 32 bits, and then we can't go beyond that, we write methods that use numbers of that length, etc. In terms of text, or strings as we often call it, we want to write functions that work on variable length strings, otherwise we would need a lot of functions! This is not a problem for numbers normally, as there are only a few common number formats (byte, word, half, double).
|
||||
|
||||
```
|
||||
Buffer overrun attacks have plagued computers for years. Recently, the Wii, Xbox and Playstation 2 all suffered buffer overrun attacks, as well as large systems like Microsoft's Web and Database servers.
|
||||
```
|
||||
|
||||
So, how do we determine how long the string is? I think the obvious answer is just to store how long the string is, and then to store the characters that make it up. This is called length prefixing, as the length comes before the string. Unfortunately, the pioneers of computer science did not agree. They felt it made more sense to have a special character called the null terminator (denoted \0) which represents when a string ends. This does indeed simplify many string algorithms, as you just keep working until the null terminator. Unfortunately this is the source of many security issues. What if a malicious user gives you a very long string? What if you didn't have enough space to store it. You might run a string copying function that copies until the null terminator, but because the string is so long, it overwrites your program. This may sound far fetched, but nevertheless, such buffer overrun attacks are incredibly common. Length prefixing mitigates this problem as it is easy to deduce the size of the buffer required to store the string. As an operating system developer, I leave it to you to decide how best to store text.
|
||||
|
||||
The next thing we need to establish is how best to map characters to numbers. Fortunately, this is reasonably well standardised, so you have two major choices, Unicode and ASCII. Unicode maps almost every single useful symbol that can be written to a number, in exchange for having a lot more numbers, and a more complicated encoding system. ASCII uses one byte per character, and so only stores the Latin alphabet, numbers, a few symbols and a few special characters. Thus, ASCII is very easy to implement, compared to Unicode, in which not every character takes the same space, making string algorithms tricky. Normally operating systems use ASCII for strings which will not be displayed to end users (but perhaps to developers or experts), and Unicode for displaying messages to users, as Unicode can support things like Japanese characters, and so could be localised.
|
||||
|
||||
Fortunately for us, this decision is irrelevant at the moment, as the first 128 characters of both are exactly the same, and are encoded exactly the same.
|
||||
|
||||
Table 1.1 ASCII/Unicode symbols 0-127
|
||||
|
||||
| | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | a | b | c | d | e | f | |
|
||||
|----| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | ----|
|
||||
| 00 | NUL | SOH | STX | ETX | EOT | ENQ | ACK | BEL | BS | HT | LF | VT | FF | CR | SO | SI | |
|
||||
| 10 | DLE | DC1 | DC2 | DC3 | DC4 | NAK | SYN | ETB | CAN | EM | SUB | ESC | FS | GS | RS | US | |
|
||||
| 20 | ! | " | # | $ | % | & | . | ( | ) | * | + | , | - | . | / | | |
|
||||
| 30 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | : | ; | < | = | > | ? | |
|
||||
| 40 | @ | A | B | C | D | E | F | G | H | I | J | K | L | M | N | O | |
|
||||
| 50 | P | Q | R | S | T | U | V | W | X | Y | Z | [ | \ | ] | ^ | _ | |
|
||||
| 60 | ` | a | b | c | d | e | f | g | h | i | j | k | l | m | n | o | |
|
||||
| 70 | p | q | r | s | t | u | v | w | x | y | z | { | | | } | ~ | DEL |
|
||||
|
||||
The table shows the first 128 symbols. The hexadecimal representation of the number for a symbol is the row value added to the column value, for example A is 4116. What you may find surprising is the first two rows, and the very last value. These 33 special characters are not printed at all. In fact, these days, many are ignored. They exist because ASCII was originally intended as a system for transmitting data over computer networks, and so a lot more information than just the symbols had to be sent. The key special symbols that you should learn are NUL, the null terminator character I mentioned before, HT, horizontal tab is what we normally refer to as a tab and LF, the line feed character is used to make a new line. You may wish to research and use the other characters for special meanings in your operating system.
|
||||
|
||||
### 2 Characters
|
||||
|
||||
So, now that we know a bit about strings, we can start to think about how they're displayed. The fundamental thing we need to do in order to be able to display a string is to be able to display a character. Our first task will be making a DrawCharacter function which takes in a character to draw and a location, and then draws the character.
|
||||
|
||||
```
|
||||
The true type font format used in many Operating Systems is so powerful, it has its own assembly language built in to ensure letters look correct at any resolution.
|
||||
```
|
||||
|
||||
Naturally, this leads to a discussion about fonts. We already know there are many ways to display any given letter in accordance with font choice. So how does a font work? In the very early days of computer science, a font was just a series of little pictures of all the letters, called a bitmap font, and all the draw character method would do is copy one of the pictures to the screen. The trouble with this is when people want to resize the text. Sometimes we need big letters, and sometimes small. Although we could keep drawing new pictures for every font at every size with every character, this would get tedious. Thus, vector fonts were invented. in vector fonts, rather than containing an image of the font, the font file contains a description of how to draw it, e.g. an 'o' could be circle with radius half that of the maximum letter height. Modern operating systems use such fonts almost exclusively, as they are perfect at any resolution.
|
||||
|
||||
Unfortunately, much though I would love to include an implementation of one of the vector font formats, it would take up the remainder of this website. Thus, we will implement a bitmap font, though if you wish to make a decent graphical operating system, a vector font would be useful.
|
||||
```
|
||||
00000000
|
||||
00000000
|
||||
00000000
|
||||
00010000
|
||||
00101000
|
||||
00101000
|
||||
00101000
|
||||
01000100
|
||||
01000100
|
||||
01111100
|
||||
11000110
|
||||
10000010
|
||||
00000000
|
||||
00000000
|
||||
00000000
|
||||
00000000
|
||||
```
|
||||
|
||||
On the downloads page, I have included several '.bin' files in the font section. These are just raw binary data files for a few fonts. For this tutorial, pick your favourite from the monospace, monochrome, 8x16 section. Download it and store it in the 'source' directory as 'font.bin'. These files are just monochrome images of each of the letters in turn, with each letter being exactly 8 by 16 pixels. Thus, each takes 16 bytes, the first byte being the top row, the second the next, etc.
|
||||
|
||||
The diagram shows the 'A' character in the monospace, monochrome, 8x16 font Bitstream Vera Sans Mono. In the file, we would find this starting at the 4116 × 1016 = 41016th byte as the following sequence in hexadecimal:
|
||||
|
||||
00, 00, 00, 10, 28, 28, 28, 44, 44, 7C, C6, 82, 00, 00, 00, 00
|
||||
|
||||
We're going to use a monospace font here, because in a monospace font every character is the same size. Unfortunately, yet another complication with most fonts is that the character's widths vary, leading to more complex display code. I've included a few other fonts on the downloads page, as well as an explanation of the format I've stored them all in.
|
||||
|
||||
So let's get down to business. Copy the following to 'drawing.s' after the .int 0 of graphicsAddress.
|
||||
|
||||
```
|
||||
.align 4
|
||||
font:
|
||||
.incbin "font.bin"
|
||||
```
|
||||
|
||||
```
|
||||
.incbin "file" inserts the binary data from the file file.
|
||||
```
|
||||
|
||||
This code copies the font data from the file to the address labelled font. We've used an .align 4 here to ensure each character starts on a multiple of 16 bytes, which can be used for a speed trick later.
|
||||
|
||||
Now we want to write the draw character method. I'll give the pseudo code for this, so you can try to implement it yourself if you want to. Conventionally >> means logical shift right.
|
||||
|
||||
```
|
||||
function drawCharacter(r0 is character, r1 is x, r2 is y)
|
||||
if character > 127 then exit
|
||||
set charAddress to font + character × 16
|
||||
for row = 0 to 15
|
||||
set bits to readByte(charAddress + row)
|
||||
for bit = 0 to 7
|
||||
if test(bits >> bit, 0x1)
|
||||
then setPixel(x + bit, y + row)
|
||||
next
|
||||
next
|
||||
return r0 = 8, r1 = 16
|
||||
end function
|
||||
|
||||
```
|
||||
If implemented directly, this is deliberately not very efficient. With things like drawing characters, efficiency is a top priority, as we will do it a lot. Let's explore some improvements that bring this closer to optimal assembly code. Firstly, we have a × 16, which by now you should spot is the same as a logical shift left by 4 places. Next we have a variable row, which is only ever added to charAddress and to y. Thus, we can eliminate it by increasing these variables instead. The only issue now is how to tell when we've finished. This is where the .align 4 comes in handy. We know that charAddress will start with the low nibble containing 0. This means we can see how far into the character data we are by checking that low nibble.
|
||||
|
||||
Though we can eliminate the need for bits, we must introduce a new variable to do so, so it is best left in. The only other improvement that can be made is to remove the nested bits >> bit.
|
||||
|
||||
```
|
||||
function drawCharacter(r0 is character, r1 is x, r2 is y)
|
||||
if character > 127 then exit
|
||||
set charAddress to font + character << 4
|
||||
loop
|
||||
set bits to readByte(charAddress)
|
||||
set bit to 8
|
||||
loop
|
||||
set bits to bits << 1
|
||||
set bit to bit - 1
|
||||
if test(bits, 0x100)
|
||||
then setPixel(x + bit, y)
|
||||
until bit = 0
|
||||
set y to y + 1
|
||||
set chadAddress to chadAddress + 1
|
||||
until charAddress AND 0b1111 = 0
|
||||
return r0 = 8, r1 = 16
|
||||
end function
|
||||
```
|
||||
|
||||
Now we've got code that is much closer to assembly code, and is near optimal. Below is the assembly code version of the above.
|
||||
|
||||
```
|
||||
.globl DrawCharacter
|
||||
DrawCharacter:
|
||||
cmp r0,#127
|
||||
movhi r0,#0
|
||||
movhi r1,#0
|
||||
movhi pc,lr
|
||||
|
||||
push {r4,r5,r6,r7,r8,lr}
|
||||
x .req r4
|
||||
y .req r5
|
||||
charAddr .req r6
|
||||
mov x,r1
|
||||
mov y,r2
|
||||
ldr charAddr,=font
|
||||
add charAddr, r0,lsl #4
|
||||
|
||||
lineLoop$:
|
||||
|
||||
bits .req r7
|
||||
bit .req r8
|
||||
ldrb bits,[charAddr]
|
||||
mov bit,#8
|
||||
|
||||
charPixelLoop$:
|
||||
|
||||
subs bit,#1
|
||||
blt charPixelLoopEnd$
|
||||
lsl bits,#1
|
||||
tst bits,#0x100
|
||||
beq charPixelLoop$
|
||||
|
||||
add r0,x,bit
|
||||
mov r1,y
|
||||
bl DrawPixel
|
||||
|
||||
teq bit,#0
|
||||
bne charPixelLoop$
|
||||
|
||||
charPixelLoopEnd$:
|
||||
.unreq bit
|
||||
.unreq bits
|
||||
add y,#1
|
||||
add charAddr,#1
|
||||
tst charAddr,#0b1111
|
||||
bne lineLoop$
|
||||
|
||||
.unreq x
|
||||
.unreq y
|
||||
.unreq charAddr
|
||||
|
||||
width .req r0
|
||||
height .req r1
|
||||
mov width,#8
|
||||
mov height,#16
|
||||
|
||||
pop {r4,r5,r6,r7,r8,pc}
|
||||
.unreq width
|
||||
.unreq height
|
||||
```
|
||||
|
||||
### 3 Strings
|
||||
|
||||
Now that we can draw characters, we can draw text. We need to make a method that, for a given string, draws each character in turn, at incrementing positions. To be nice, we shall also implement new lines and tabs. It's decision time as far as null terminators are concerned, and if you want to make your operating system use them, feel free by changing the code below. To avoid the issue, I will have the length of the string passed as an argument to the DrawString function, along with the address of the string, and the x and y coordinates.
|
||||
|
||||
```
|
||||
function drawString(r0 is string, r1 is length, r2 is x, r3 is y)
|
||||
set x0 to x
|
||||
for pos = 0 to length - 1
|
||||
set char to loadByte(string + pos)
|
||||
set (cwidth, cheight) to DrawCharacter(char, x, y)
|
||||
if char = '\n' then
|
||||
set x to x0
|
||||
set y to y + cheight
|
||||
otherwise if char = '\t' then
|
||||
set x1 to x
|
||||
until x1 > x0
|
||||
set x1 to x1 + 5 × cwidth
|
||||
loop
|
||||
set x to x1
|
||||
otherwise
|
||||
set x to x + cwidth
|
||||
end if
|
||||
next
|
||||
end function
|
||||
```
|
||||
|
||||
Once again, this function isn't that close to assembly code. Feel free to try to implement it either directly or by simplifying it. I will give the simplification and then the assembly code below.
|
||||
|
||||
Clearly the person who wrote this function wasn't being very efficient (me in case you were wondering). Once again we have a pos variable that just increments and is added to something else, which is completely unnecessary. We can remove it, and instead simultaneously decrement length until it is 0, saving the need for one register. The rest of the function is probably fine, except for that annoying multiplication by five. A key thing to do here would be to move the multiplication outside the loop; multiplication is slow even with bit shifts, and since we're always adding the same constant multiplied by 5, there is no need to recompute this. It can in fact be implemented in one operation using the argument shifting in assembly code, so I shall rephrase it like that.
|
||||
|
||||
```
|
||||
function drawString(r0 is string, r1 is length, r2 is x, r3 is y)
|
||||
set x0 to x
|
||||
until length = 0
|
||||
set length to length - 1
|
||||
set char to loadByte(string)
|
||||
set (cwidth, cheight) to DrawCharacter(char, x, y)
|
||||
if char = '\n' then
|
||||
set x to x0
|
||||
set y to y + cheight
|
||||
otherwise if char = '\t' then
|
||||
set x1 to x
|
||||
set cwidth to cwidth + cwidth << 2
|
||||
until x1 > x0
|
||||
set x1 to x1 + cwidth
|
||||
loop
|
||||
set x to x1
|
||||
otherwise
|
||||
set x to x + cwidth
|
||||
end if
|
||||
set string to string + 1
|
||||
loop
|
||||
end function
|
||||
```
|
||||
|
||||
In assembly code:
|
||||
|
||||
```
|
||||
.globl DrawString
|
||||
DrawString:
|
||||
x .req r4
|
||||
y .req r5
|
||||
x0 .req r6
|
||||
string .req r7
|
||||
length .req r8
|
||||
char .req r9
|
||||
push {r4,r5,r6,r7,r8,r9,lr}
|
||||
|
||||
mov string,r0
|
||||
mov x,r2
|
||||
mov x0,x
|
||||
mov y,r3
|
||||
mov length,r1
|
||||
|
||||
stringLoop$:
|
||||
subs length,#1
|
||||
blt stringLoopEnd$
|
||||
|
||||
ldrb char,[string]
|
||||
add string,#1
|
||||
|
||||
mov r0,char
|
||||
mov r1,x
|
||||
mov r2,y
|
||||
bl DrawCharacter
|
||||
cwidth .req r0
|
||||
cheight .req r1
|
||||
|
||||
teq char,#'\n'
|
||||
moveq x,x0
|
||||
addeq y,cheight
|
||||
beq stringLoop$
|
||||
|
||||
teq char,#'\t'
|
||||
addne x,cwidth
|
||||
bne stringLoop$
|
||||
|
||||
add cwidth, cwidth,lsl #2
|
||||
x1 .req r1
|
||||
mov x1,x0
|
||||
|
||||
stringLoopTab$:
|
||||
add x1,cwidth
|
||||
cmp x,x1
|
||||
bge stringLoopTab$
|
||||
mov x,x1
|
||||
.unreq x1
|
||||
b stringLoop$
|
||||
stringLoopEnd$:
|
||||
.unreq cwidth
|
||||
.unreq cheight
|
||||
|
||||
pop {r4,r5,r6,r7,r8,r9,pc}
|
||||
.unreq x
|
||||
.unreq y
|
||||
.unreq x0
|
||||
.unreq string
|
||||
.unreq length
|
||||
```
|
||||
|
||||
```
|
||||
subs reg,#val subtracts val from the register reg and compares the result with 0.
|
||||
```
|
||||
|
||||
This code makes clever use of a new operation, subs which subtracts one number from another, stores the result and then compares it with 0. In truth, all comparisons are implemented as a subtraction and then comparison with 0, but the result is normally discarded. This means that this operation is as fast as cmp.
|
||||
|
||||
### 4 Your Wish is My Command Line
|
||||
|
||||
Now that we can print strings, the challenge is to find an interesting one to draw. Normally in tutorials such as this, people just draw "Hello World!", but after all we've done so far, I feel that is a little patronising (feel free to do so if it helps). Instead we're going to draw our command line.
|
||||
|
||||
A convention has been made for computers running ARM. When they boot, it is important they are given certain information about what they have available to them. Most all processors have some way of ascertaining this information, and on ARM this is by data left at the address 10016. The format of the data is as follows:
|
||||
|
||||
1. The data is broken down into a series of 'tags'.
|
||||
2. There are nine types of tag: 'core', 'mem', 'videotext', 'ramdisk', 'initrd2', 'serial' 'revision', 'videolfb', 'cmdline'.
|
||||
3. Each can only appear once, but all but the 'core' tag don't have to appear.
|
||||
4. The tags are placed from 0x100 in order one after the other.
|
||||
5. The end of the list of tags always contains 2 words which are 0.
|
||||
6. Every tag's size in bytes is a multiple of 4.
|
||||
7. Each tag starts with the size of the tag in words in the tag, including this number.
|
||||
8. This is followed by a half word containing the tag's number. These are numbered from 1 in the order above ('core' is 1, 'cmdline' is 9).
|
||||
9. This is followed by a half word containing 544116.
|
||||
10. After this comes the data of the tag, which varies depending on the tag. The size of the data in words + 2 is always the same as the length mentioned above.
|
||||
11. A 'core' tag is either 2 or 5 words in length. If it is 2, there is no data, if it is 5, it has 3 words.
|
||||
12. A 'mem' tag is always 4 words in length. The data is the first address in a block of memory, and the length of that block.
|
||||
13. A 'cmdline' tag contains a null terminated string which is the parameters of the kernel.
|
||||
|
||||
|
||||
```
|
||||
Almost all Operating Systems support the notion of programs having a 'command line'. The idea is to provide a common mechanism for choosing the desired behaviour of the program.
|
||||
```
|
||||
|
||||
On the current version of the Raspberry Pi, only the 'core', 'mem' and 'cmdline' tags are present. You may find these useful later, and a more complete reference for these is on our Raspberry Pi reference page. The one we're interested in at the moment is the 'cmdline' tag, because it contains a string. We're going to write some code to search for the command line tag, and, if found, to print it out with each item on a new line. The command line is just a list of things that either the graphics processor or the user thought it might be nice for the Operating System to know. On the Raspberry Pi, this includes the MAC Address, serial number and screen resolution. The string itself is just a list of expressions such as 'key.subkey=value' separated by spaces.
|
||||
|
||||
Let's start by finding the 'cmdline' tag. In a new file called 'tags.s' copy the following code.
|
||||
|
||||
```
|
||||
.section .data
|
||||
tag_core: .int 0
|
||||
tag_mem: .int 0
|
||||
tag_videotext: .int 0
|
||||
tag_ramdisk: .int 0
|
||||
tag_initrd2: .int 0
|
||||
tag_serial: .int 0
|
||||
tag_revision: .int 0
|
||||
tag_videolfb: .int 0
|
||||
tag_cmdline: .int 0
|
||||
```
|
||||
|
||||
Looking through the list of tags will be a slow operation, as it involves a lot of memory access. Therefore, we only want to have to do it once. This code creates some data which will store the memory address of the first tag of each of the types. Then, to find a tag the following pseudo code will suffice.
|
||||
|
||||
```
|
||||
function FindTag(r0 is tag)
|
||||
if tag > 9 or tag = 0 then return 0
|
||||
set tagAddr to loadWord(tag_core + (tag - 1) × 4)
|
||||
if not tagAddr = 0 then return tagAddr
|
||||
if readWord(tag_core) = 0 then return 0
|
||||
set tagAddr to 0x100
|
||||
loop forever
|
||||
set tagIndex to readHalfWord(tagAddr + 4)
|
||||
if tagIndex = 0 then return FindTag(tag)
|
||||
if readWord(tag_core+(tagIndex-1)×4) = 0
|
||||
then storeWord(tagAddr, tag_core+(tagIndex-1)×4)
|
||||
set tagAddr to tagAddr + loadWord(tagAddr) × 4
|
||||
end loop
|
||||
end function
|
||||
```
|
||||
This code is already quite well optimised and close to assembly. It is optimistic in that the first thing it tries is loading the tag directly, as all but the first time this should be the case. If that fails, it checks if the core tag has an address. Since there must always be a core tag, the only reason that it would not have an address is if it doesn't exist. If it does have an address, the tag we were looking for didn't. If it doesn't we need to find the addresses of all the tags. It does this by reading the number of the tag. If it is zero, that must mean we are at the end of the list. This means we've now filled in all the tags in our directory. Therefore if we run our function again, it will now be able to produce an answer. If the tag number is not zero, we check to see if this tag type already has an address. If not, we store the address of this tag in our directory. We then add the length of this tag in bytes to the tag address to find the next tag.
|
||||
|
||||
Have a go at implementing this code in assembly. You will need to simplify it. If you get stuck, my answer is below. Don't forget the .section .text!
|
||||
|
||||
```
|
||||
.section .text
|
||||
.globl FindTag
|
||||
FindTag:
|
||||
tag .req r0
|
||||
tagList .req r1
|
||||
tagAddr .req r2
|
||||
|
||||
sub tag,#1
|
||||
cmp tag,#8
|
||||
movhi tag,#0
|
||||
movhi pc,lr
|
||||
|
||||
ldr tagList,=tag_core
|
||||
tagReturn$:
|
||||
add tagAddr,tagList, tag,lsl #2
|
||||
ldr tagAddr,[tagAddr]
|
||||
|
||||
teq tagAddr,#0
|
||||
movne r0,tagAddr
|
||||
movne pc,lr
|
||||
|
||||
ldr tagAddr,[tagList]
|
||||
teq tagAddr,#0
|
||||
movne r0,#0
|
||||
movne pc,lr
|
||||
|
||||
mov tagAddr,#0x100
|
||||
push {r4}
|
||||
tagIndex .req r3
|
||||
oldAddr .req r4
|
||||
tagLoop$:
|
||||
ldrh tagIndex,[tagAddr,#4]
|
||||
subs tagIndex,#1
|
||||
poplt {r4}
|
||||
blt tagReturn$
|
||||
|
||||
add tagIndex,tagList, tagIndex,lsl #2
|
||||
ldr oldAddr,[tagIndex]
|
||||
teq oldAddr,#0
|
||||
.unreq oldAddr
|
||||
streq tagAddr,[tagIndex]
|
||||
|
||||
ldr tagIndex,[tagAddr]
|
||||
add tagAddr, tagIndex,lsl #2
|
||||
b tagLoop$
|
||||
|
||||
.unreq tag
|
||||
.unreq tagList
|
||||
.unreq tagAddr
|
||||
.unreq tagIndex
|
||||
```
|
||||
|
||||
### 5 Hello World
|
||||
|
||||
Now that we have everything we need, we can draw our first string. In 'main.s' delete everything after bl SetGraphicsAddress, and replace it with the following:
|
||||
|
||||
```
|
||||
mov r0,#9
|
||||
bl FindTag
|
||||
ldr r1,[r0]
|
||||
lsl r1,#2
|
||||
sub r1,#8
|
||||
add r0,#8
|
||||
mov r2,#0
|
||||
mov r3,#0
|
||||
bl DrawString
|
||||
loop$:
|
||||
b loop$
|
||||
```
|
||||
|
||||
This code simply uses our FindTag method to find the 9th tag (cmdline) and then calculates its length and passes the command and the length to the DrawString method, and tells it to draw the string at 0,0. Now test this on the Raspberry Pi. You should see a line of text on the screen. If not please see our troubleshooting page.
|
||||
|
||||
Once it works, congratulations you've now got the ability to draw text. But there is still room for improvement. What if we wanted to write out a number, or a section of the memory or manipulate our command line? In [Lesson 9: Screen04][2], we will look at manipulating text and displaying useful numbers and information.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
via: https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/screen03.html
|
||||
|
||||
作者:[Alex Chadwick][a]
|
||||
选题:[lujun9972][b]
|
||||
译者:[译者ID](https://github.com/译者ID)
|
||||
校对:[校对者ID](https://github.com/校对者ID)
|
||||
|
||||
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
|
||||
|
||||
[a]: https://www.cl.cam.ac.uk
|
||||
[b]: https://github.com/lujun9972
|
||||
[1]: https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/screen02.html
|
||||
[2]: https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/screen04.html
|
@ -1,5 +1,5 @@
|
||||
[#]: collector: (lujun9972)
|
||||
[#]: translator: ( )
|
||||
[#]: translator: (qhwdw)
|
||||
[#]: reviewer: ( )
|
||||
[#]: publisher: ( )
|
||||
[#]: url: ( )
|
||||
|
@ -0,0 +1,469 @@
|
||||
[#]: collector: (lujun9972)
|
||||
[#]: translator: (qhwdw)
|
||||
[#]: reviewer: ( )
|
||||
[#]: publisher: ( )
|
||||
[#]: url: ( )
|
||||
[#]: subject: (Computer Laboratory – Raspberry Pi: Lesson 8 Screen03)
|
||||
[#]: via: (https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/screen03.html)
|
||||
[#]: author: (Alex Chadwick https://www.cl.cam.ac.uk)
|
||||
|
||||
计算机实验室 – 树莓派:课程 8 屏幕03
|
||||
======
|
||||
|
||||
屏幕03 课程基于屏幕02 课程来构建,它教你如何绘制文本,和一个操作系统命令行参数上的一个小特性。假设你已经有了[课程 7:屏幕02][1] 的操作系统代码,我们将以它为基础来构建。
|
||||
|
||||
### 1、字符串的理论知识
|
||||
|
||||
是的,我们的任务是为这个操作系统绘制文本。我们有几个问题需要去处理,最紧急的那个可能是如何去保存文本。令人难以置信的是,文本是迄今为止在计算机上最大的缺陷之一。原本应该是简单的数据类型却导致了操作系统的崩溃,破坏了完美的加密,并给使用不同字母表的用户带来了许多问题。尽管如此,它仍然是极其重要的数据类型,因为它将计算机和用户很好地连接起来。文本是计算机能够理解的非常好的结构,同时人类使用它时也有足够的可读性。
|
||||
|
||||
```
|
||||
可变数据类型,比如文本要求能够进行很复杂的处理。
|
||||
```
|
||||
|
||||
那么,文本是如何保存的呢?非常简单,我们使用一种方法,给每个字母分配一个唯一的编号,然后我们保存一系列的这种编号。看起来很容易吧。问题是,那个编号的数字是不固定的。一些文本片断可能比其它的长。与保存普通数字一样,我们有一些固有的限制,即:3 位,我们不能超过这个限制,我们添加方法去使用那种长数字等等。“文本”这个术语,我们经常也叫它“字符串”,我们希望能够写一个可用于变长字符串的函数,否则就需要写很多函数!对于一般的数字来说,这不是个问题,因为只有几种通用的数字格式(字节、字、半字节、双字节)。
|
||||
|
||||
```
|
||||
缓冲区溢出攻击祸害计算机由来已久。最近,Wii、Xbox 和 Playstation 2、以及大型系统如 Microsoft 的 Web 和数据库服务器,都遭受到缓冲区溢出攻击。
|
||||
```
|
||||
|
||||
因此,如何判断字符串长度?我想显而易见的答案是存储多长的字符串,然后去存储组成字符串的字符。这称为长度前缀,因为长度位于字符串的前面。不幸的是,计算机科学家的先驱们不同意这么做。他们认为使用一个称为空终止符(NULL)的特殊字符(用 \0表示)来表示字符串结束更有意义。这样确定简化了许多字符串算法,因为你只需要持续操作直到遇到空终止符为止。不幸的是,这成为了许多安全问题的根源。如果一个恶意用户给你一个特别长的字符串会发生什么状况?如果没有足够的空间去保存这个特别长的字符串会发生什么状况?你可以使用一个字符串复制函数来做复制,直到遇到空终止符为止,但是因为字符串特别长,而覆写了你的程序,怎么办?这看上去似乎有些较真,但尽管如此,缓冲区溢出攻击还是经常发生。长度前缀可以很容易地缓解这种问题,因为它可以很容易地推算出保存这个字符串所需要的缓冲区的长度。作为一个操作系统开发者,我留下这个问题,由你去决定如何才能更好地存储文本。
|
||||
|
||||
接下来的事情是,我们需要去维护一个很好的从字符到数字的映射。幸运的是,这是高度标准化的,我们有两个主要的选择,Unicode 和 ASCII。Unicode 几乎将每个单个的有用的符号都映射为数字,作为交换,我们得到的是很多很多的数字,和一个更复杂的编码方法。ASCII 为每个字符使用一个字节,因此它仅保存拉丁字母、数字、少数符号和少数特殊字符。因此,ASCII 是非常易于实现的,与 Unicode 相比,它的每个字符占用的空间并不相同,这使得字符串算法更棘手。一般操作系统上字符使用 ASCII,并不是为了显示给最终用户的(开发者和专家用户除外),给终端用户显示信息使用 Unicode,因为 Unicode 能够支持像日语字符这样的东西,并且因此可以实现本地化。
|
||||
|
||||
幸运的是,在这里我们不需要去做选择,因为它们的前 128 个字符是完全相同的,并且编码也是完全一样的。
|
||||
|
||||
表 1.1 ASCII/Unicode 符号 0-127
|
||||
|
||||
| | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | a | b | c | d | e | f | |
|
||||
|----| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | ----|
|
||||
| 00 | NUL | SOH | STX | ETX | EOT | ENQ | ACK | BEL | BS | HT | LF | VT | FF | CR | SO | SI | |
|
||||
| 10 | DLE | DC1 | DC2 | DC3 | DC4 | NAK | SYN | ETB | CAN | EM | SUB | ESC | FS | GS | RS | US | |
|
||||
| 20 | ! | " | # | $ | % | & | . | ( | ) | * | + | , | - | . | / | | |
|
||||
| 30 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | : | ; | < | = | > | ? | |
|
||||
| 40 | @ | A | B | C | D | E | F | G | H | I | J | K | L | M | N | O | |
|
||||
| 50 | P | Q | R | S | T | U | V | W | X | Y | Z | [ | \ | ] | ^ | _ | |
|
||||
| 60 | ` | a | b | c | d | e | f | g | h | i | j | k | l | m | n | o | |
|
||||
| 70 | p | q | r | s | t | u | v | w | x | y | z | { | | | } | ~ | DEL |
|
||||
|
||||
这个表显示了前 128 个符号。一个符号的十六进制表示是行的值加上列的值,比如 A 是 41~16~。你可以惊奇地发现前两行和最后的值。这 33 个特殊字符是不可打印字符。事实上,许多人都忽略了它们。它们之所以存在是因为 ASCII 最初设计是基于计算机网络来传输数据的一种方法。因此它要发送的信息不仅仅是符号。你应该学习的重要的特殊字符是 `NUL`,它就是我们前面提到的空终止符。`HT` 水平制表符就是我们经常说的 `tab`,而 `LF` 换行符用于生成一个新行。你可能想研究和使用其它特殊字符在你的操行系统中的意义。
|
||||
|
||||
### 2、字符
|
||||
|
||||
到目前为止,我们已经知道了一些关于字符串的知识,我们可以开始想想它们是如何显示的。为了显示一个字符串,我们需要做的最基础的事情是能够显示一个字符。我们的第一个任务是编写一个 `DrawCharacter` 函数,给它一个要绘制的字符和一个位置,然后它将这个字符绘制出来。
|
||||
|
||||
```markdown
|
||||
在许多操作系统中使用的 `truetype` 字体格式是很强大的,它内置有它自己的汇编语言,以确保在任何分辨率下字母看起来都是正确的。
|
||||
```
|
||||
|
||||
这就很自然地引出关于字体的讨论。我们已经知道有许多方式去按照选定的字体去显示任何给定的字母。那么字体又是如何工作的呢?在计算机科学的早期阶段,一种字体就是所有字母的一系列小图片而已,这种字体称为位图字体,而所有的字符绘制方法就是将图片复制到屏幕上。当人们想去调整字体大小时就出问题了。有时我们需要大的字母,而有时我们需要的是小的字母。尽管我们可以为每个字体、每种大小、每个字符都绘制新图片,但这种作法过于单调乏味。所以,发明了矢量字体。矢量字体不包含字体的图像,它包含的是如何去绘制字符的描述,即:一个 `o` 可能是最大字母高度的一半为半径绘制的圆。现代操作系统都几乎仅使用这种字体,因为这种字体在任何分辨率下都很完美。
|
||||
|
||||
不幸的是,虽然我很想包含一个矢量字体的格式的实现,但它的内容太多了,将占用这个站点的剩余部分。所以,我们将去实现一个位图字体,可是,如果你想去做一个正宗的图形化的操作系统,那么矢量字体将是很有用的。
|
||||
|
||||
在下载页面上的字体节中,我们提供了几个 `.bin` 文件。这些只是字体的原始二进制数据文件。为完成本教程,从等宽、单色、8x16 节中挑选你喜欢的字体。然后下载它并保存到 `source` 目录中并命名为 `font.bin` 文件。这些文件只是每个字母的单色图片,它们每个字母刚好是 8 x 16 个像素。所以,每个字母占用 16 字节,第一个字节是第一行,第二个字节是第二行,依此类推。
|
||||
|
||||
![bitmap](https://ws2.sinaimg.cn/large/006tNc79ly1fzzb2064agj305l0apt96.jpg)
|
||||
|
||||
这个示意图展示了等宽、单色、8x16 的字符 A 的 `Bitstream Vera Sans Mono`。在这个文件中,我们可以找到,它从第 41~16~ × 10~16~ = 410~16~ 字节开始的十六进制序列:
|
||||
|
||||
00, 00, 00, 10, 28, 28, 28, 44, 44, 7C, C6, 82, 00, 00, 00, 00
|
||||
|
||||
在这里我们将使用等宽字体,因为等宽字体的每个字符大小是相同的。不幸的是,大多数字体的复杂之处就是因为它的宽度不同,从而导致它的显示代码更复杂。在下载页面上还包含有几个其它的字体,并包含了这种字体的存储格式介绍。
|
||||
|
||||
我们回到正题。复制下列代码到 `drawing.s` 中的 `graphicsAddress` 的 `.int 0` 之后。
|
||||
|
||||
```assembly
|
||||
.align 4
|
||||
font:
|
||||
.incbin "font.bin"
|
||||
```
|
||||
|
||||
```assembly
|
||||
.incbin "file" 插入来自文件 “file” 中的二进制数据。
|
||||
```
|
||||
|
||||
这段代码复制文件中的字体数据到标签为 `font` 的地址。我们在这里使用了一个 `.align 4` 去确保每个字符都是从 16 字节的倍数开始,这是一个以后经常用到的用于加快访问速度的技巧。
|
||||
|
||||
现在我们去写绘制字符的方法。我在下面给出了伪代码,你可以尝试自己去实现它。按惯例 `>>` 的意思是逻辑右移。
|
||||
|
||||
```c
|
||||
function drawCharacter(r0 is character, r1 is x, r2 is y)
|
||||
if character > 127 then exit
|
||||
set charAddress to font + character × 16
|
||||
for row = 0 to 15
|
||||
set bits to readByte(charAddress + row)
|
||||
for bit = 0 to 7
|
||||
if test(bits >> bit, 0x1)
|
||||
then setPixel(x + bit, y + row)
|
||||
next
|
||||
next
|
||||
return r0 = 8, r1 = 16
|
||||
end function
|
||||
|
||||
```
|
||||
如果直接去实现它,这显然不是个高效率的做法。像绘制字符这样的事情,效率是最重要的。因为我们要频繁使用它。我们来探索一些改善的方法,使其成为最优化的汇编代码。首先,因为我们有一个 `× 16`,你应该会马上想到它等价于逻辑左移 4 位。紧接着我们有一个变量 `row`,它只与 `charAddress` 和 `y` 相加。所以,我们可以通过增加替代变量来消除它。现在唯一的问题是如何判断我们何时完成。这时,一个很好用的 `.align 4` 上场了。我们知道,`charAddress` 将从包含 0 的低位半字节开始。这意味着我们可以通过检查低位半字节来看到进入字符数据的程度。
|
||||
|
||||
虽然我们可以消除对 `bit` 的需求,但我们必须要引入新的变量才能实现,因此最好还是保留它。剩下唯一的改进就是去除嵌套的 `bits >> bit`。
|
||||
|
||||
```c
|
||||
function drawCharacter(r0 is character, r1 is x, r2 is y)
|
||||
if character > 127 then exit
|
||||
set charAddress to font + character << 4
|
||||
loop
|
||||
set bits to readByte(charAddress)
|
||||
set bit to 8
|
||||
loop
|
||||
set bits to bits << 1
|
||||
set bit to bit - 1
|
||||
if test(bits, 0x100)
|
||||
then setPixel(x + bit, y)
|
||||
until bit = 0
|
||||
set y to y + 1
|
||||
set chadAddress to chadAddress + 1
|
||||
until charAddress AND 0b1111 = 0
|
||||
return r0 = 8, r1 = 16
|
||||
end function
|
||||
```
|
||||
|
||||
现在,我们已经得到了非常接近汇编代码的代码了,并且代码也是经过优化的。下面就是上述代码用汇编写出来的代码。
|
||||
|
||||
```assembly
|
||||
.globl DrawCharacter
|
||||
DrawCharacter:
|
||||
cmp r0,#127
|
||||
movhi r0,#0
|
||||
movhi r1,#0
|
||||
movhi pc,lr
|
||||
|
||||
push {r4,r5,r6,r7,r8,lr}
|
||||
x .req r4
|
||||
y .req r5
|
||||
charAddr .req r6
|
||||
mov x,r1
|
||||
mov y,r2
|
||||
ldr charAddr,=font
|
||||
add charAddr, r0,lsl #4
|
||||
|
||||
lineLoop$:
|
||||
|
||||
bits .req r7
|
||||
bit .req r8
|
||||
ldrb bits,[charAddr]
|
||||
mov bit,#8
|
||||
|
||||
charPixelLoop$:
|
||||
|
||||
subs bit,#1
|
||||
blt charPixelLoopEnd$
|
||||
lsl bits,#1
|
||||
tst bits,#0x100
|
||||
beq charPixelLoop$
|
||||
|
||||
add r0,x,bit
|
||||
mov r1,y
|
||||
bl DrawPixel
|
||||
|
||||
teq bit,#0
|
||||
bne charPixelLoop$
|
||||
|
||||
charPixelLoopEnd$:
|
||||
.unreq bit
|
||||
.unreq bits
|
||||
add y,#1
|
||||
add charAddr,#1
|
||||
tst charAddr,#0b1111
|
||||
bne lineLoop$
|
||||
|
||||
.unreq x
|
||||
.unreq y
|
||||
.unreq charAddr
|
||||
|
||||
width .req r0
|
||||
height .req r1
|
||||
mov width,#8
|
||||
mov height,#16
|
||||
|
||||
pop {r4,r5,r6,r7,r8,pc}
|
||||
.unreq width
|
||||
.unreq height
|
||||
```
|
||||
|
||||
### 3、字符串
|
||||
|
||||
现在,我们可以绘制字符了,我们可以绘制文本了。我们需要去写一个方法,给它一个字符串为输入,它通过递增位置来绘制出每个字符。为了做的更好,我们应该去实现新的行和制表符。是时候决定关于空终止符的问题了,如果你想让你的操作系统使用它们,可以按需来修改下面的代码。为避免这个问题,我将给 `DrawString` 函数传递一个字符串长度,以及字符串的地址,和 x 和 y 的坐标作为参数。
|
||||
|
||||
```c
|
||||
function drawString(r0 is string, r1 is length, r2 is x, r3 is y)
|
||||
set x0 to x
|
||||
for pos = 0 to length - 1
|
||||
set char to loadByte(string + pos)
|
||||
set (cwidth, cheight) to DrawCharacter(char, x, y)
|
||||
if char = '\n' then
|
||||
set x to x0
|
||||
set y to y + cheight
|
||||
otherwise if char = '\t' then
|
||||
set x1 to x
|
||||
until x1 > x0
|
||||
set x1 to x1 + 5 × cwidth
|
||||
loop
|
||||
set x to x1
|
||||
otherwise
|
||||
set x to x + cwidth
|
||||
end if
|
||||
next
|
||||
end function
|
||||
```
|
||||
|
||||
同样,这个函数与汇编代码还有很大的差距。你可以随意去尝试实现它,即可以直接实现它,也可以简化它。我在下面给出了简化后的函数和汇编代码。
|
||||
|
||||
很明显,写这个函数的人并不很有效率(感到奇怪吗?它就是我写的)。再说一次,我们有一个 `pos` 变量,它用于递增和与其它东西相加,这是完全没有必要的。我们可以去掉它,而同时进行长度递减,直到减到 0 为止,这样就少用了一个寄存器。除了那个烦人的乘以 5 以外,函数的其余部分还不错。在这里要做的一个重要事情是,将乘法移到循环外面;即便使用位移运算,乘法仍然是很慢的,由于我们总是加一个乘以 5 的相同的常数,因此没有必要重新计算它。实际上,在汇编代码中它可以在一个操作数中通过参数移位来实现,因此我将代码改变为下面这样。
|
||||
|
||||
```c
|
||||
function drawString(r0 is string, r1 is length, r2 is x, r3 is y)
|
||||
set x0 to x
|
||||
until length = 0
|
||||
set length to length - 1
|
||||
set char to loadByte(string)
|
||||
set (cwidth, cheight) to DrawCharacter(char, x, y)
|
||||
if char = '\n' then
|
||||
set x to x0
|
||||
set y to y + cheight
|
||||
otherwise if char = '\t' then
|
||||
set x1 to x
|
||||
set cwidth to cwidth + cwidth << 2
|
||||
until x1 > x0
|
||||
set x1 to x1 + cwidth
|
||||
loop
|
||||
set x to x1
|
||||
otherwise
|
||||
set x to x + cwidth
|
||||
end if
|
||||
set string to string + 1
|
||||
loop
|
||||
end function
|
||||
```
|
||||
|
||||
以下是它的汇编代码:
|
||||
|
||||
```assembly
|
||||
.globl DrawString
|
||||
DrawString:
|
||||
x .req r4
|
||||
y .req r5
|
||||
x0 .req r6
|
||||
string .req r7
|
||||
length .req r8
|
||||
char .req r9
|
||||
push {r4,r5,r6,r7,r8,r9,lr}
|
||||
|
||||
mov string,r0
|
||||
mov x,r2
|
||||
mov x0,x
|
||||
mov y,r3
|
||||
mov length,r1
|
||||
|
||||
stringLoop$:
|
||||
subs length,#1
|
||||
blt stringLoopEnd$
|
||||
|
||||
ldrb char,[string]
|
||||
add string,#1
|
||||
|
||||
mov r0,char
|
||||
mov r1,x
|
||||
mov r2,y
|
||||
bl DrawCharacter
|
||||
cwidth .req r0
|
||||
cheight .req r1
|
||||
|
||||
teq char,#'\n'
|
||||
moveq x,x0
|
||||
addeq y,cheight
|
||||
beq stringLoop$
|
||||
|
||||
teq char,#'\t'
|
||||
addne x,cwidth
|
||||
bne stringLoop$
|
||||
|
||||
add cwidth, cwidth,lsl #2
|
||||
x1 .req r1
|
||||
mov x1,x0
|
||||
|
||||
stringLoopTab$:
|
||||
add x1,cwidth
|
||||
cmp x,x1
|
||||
bge stringLoopTab$
|
||||
mov x,x1
|
||||
.unreq x1
|
||||
b stringLoop$
|
||||
stringLoopEnd$:
|
||||
.unreq cwidth
|
||||
.unreq cheight
|
||||
|
||||
pop {r4,r5,r6,r7,r8,r9,pc}
|
||||
.unreq x
|
||||
.unreq y
|
||||
.unreq x0
|
||||
.unreq string
|
||||
.unreq length
|
||||
```
|
||||
|
||||
```assembly
|
||||
subs reg,#val 从寄存器 reg 中减去 val,然后将结果与 0 进行比较。
|
||||
```
|
||||
|
||||
这个代码中非常聪明地使用了一个新运算,`subs` 是从一个操作数中减去另一个数,保存结果,然后将结果与 0 进行比较。实现上,所有的比较都可以实现为减法后的结果与 0 进行比较,但是结果通常会丢弃。这意味着这个操作与 `cmp` 一样快。
|
||||
|
||||
### 4、你的愿意是我的命令行
|
||||
|
||||
现在,我们可以输出字符串了,而挑战是找到一个有意思的字符串去绘制。一般在这样的教程中,人们都希望去绘制 “Hello World!”,但是到目前为止,虽然我们已经能做到了,我觉得这有点“君临天下”的感觉(如果喜欢这种感觉,请随意!)。因此,作为替代,我们去继续绘制我们的命令行。
|
||||
|
||||
有一个限制是我们所做的操作系统是用在 ARM 架构的计算机上。最关键的是,在它们引导时,给它一些信息告诉它有哪些可用资源。几乎所有的处理器都有某些方式来确定这些信息,而在 ARM 上,它是通过位于地址 100<sub>16</sub> 处的数据来确定的,这个数据的格式如下:
|
||||
|
||||
1. 数据是可分解的一系列的标签。
|
||||
2. 这里有九种类型的标签:`core`,`mem`,`videotext`,`ramdisk`,`initrd2`,`serial`,`revision`,`videolfb`,`cmdline`。
|
||||
3. 每个标签只能出现一次,除了 'core’ 标签是必不可少的之外,其它的都是可有可无的。
|
||||
4. 所有标签都依次放置在地址 0x100 处。
|
||||
5. 标签列表的结束处总是有两个<ruby>字<rt>word</rt></ruby>,它们全为 0。
|
||||
6. 每个标签的字节数都是 4 的倍数。
|
||||
7. 每个标签都是以标签中(以字为单位)的标签大小开始(标签包含这个数字)。
|
||||
8. 紧接着是包含标签编号的一个半字。编号是按上面列出的顺序,从 1 开始(`core` 是 1,`cmdline` 是 9)。
|
||||
9. 紧接着是一个包含 5441<sub>16</sub> 的半字。
|
||||
10. 之后是标签的数据,它根据标签不同是可变的。数据大小(以字为单位)+ 2 的和总是与前面提到的长度相同。
|
||||
11. 一个 `core` 标签的长度可以是 2 个字也可以是 5 个字。如果是 2 个字,表示没有数据,如果是 5 个字,表示它有 3 个字的数据。
|
||||
12. 一个 `mem` 标签总是 4 个字的长度。数据是内存块的第一个地址,和内存块的长度。
|
||||
13. 一个 `cmdline` 标签包含一个 `null` 终止符字符串,它是个内核参数。
|
||||
|
||||
|
||||
```markdown
|
||||
几乎所有的操作系统都支持一个`命令行`的程序。它的想法是为选择一个程序所期望的行为而提供一个通用的机制。
|
||||
```
|
||||
|
||||
在目前的树莓派版本中,只提供了 `core`、`mem` 和 `cmdline` 标签。你可以在后面找到它们的用法,更全面的参考资料在树莓派的参考页面上。现在,我们感兴趣的是 `cmdline` 标签,因为它包含一个字符串。我们继续写一些搜索命令行标签的代码,如果找到了,以每个条目一个新行的形式输出它。命令行只是为了让操作系统理解图形处理器或用户认为的很好的事情的一个列表。在树莓派上,这包含了 MAC 地址,序列号和屏幕分辨率。字符串本身也是一个像 `key.subkey=value` 这样的由空格隔开的表达式列表。
|
||||
|
||||
我们从查找 `cmdline` 标签开始。将下列的代码复制到一个名为 `tags.s` 的新文件中。
|
||||
|
||||
```assembly
|
||||
.section .data
|
||||
tag_core: .int 0
|
||||
tag_mem: .int 0
|
||||
tag_videotext: .int 0
|
||||
tag_ramdisk: .int 0
|
||||
tag_initrd2: .int 0
|
||||
tag_serial: .int 0
|
||||
tag_revision: .int 0
|
||||
tag_videolfb: .int 0
|
||||
tag_cmdline: .int 0
|
||||
```
|
||||
|
||||
通过标签列表来查找是一个很慢的操作,因为这涉及到许多内存访问。因此,我们只是想实现它一次。代码创建一些数据,用于保存每个类型的第一个标签的内存地址。接下来,用下面的伪代码就可以找到一个标签了。
|
||||
|
||||
```c
|
||||
function FindTag(r0 is tag)
|
||||
if tag > 9 or tag = 0 then return 0
|
||||
set tagAddr to loadWord(tag_core + (tag - 1) × 4)
|
||||
if not tagAddr = 0 then return tagAddr
|
||||
if readWord(tag_core) = 0 then return 0
|
||||
set tagAddr to 0x100
|
||||
loop forever
|
||||
set tagIndex to readHalfWord(tagAddr + 4)
|
||||
if tagIndex = 0 then return FindTag(tag)
|
||||
if readWord(tag_core+(tagIndex-1)×4) = 0
|
||||
then storeWord(tagAddr, tag_core+(tagIndex-1)×4)
|
||||
set tagAddr to tagAddr + loadWord(tagAddr) × 4
|
||||
end loop
|
||||
end function
|
||||
```
|
||||
这段代码已经是优化过的,并且很接近汇编了。它尝试直接加载标签,第一次这样做是有些乐观的,但是除了第一次之外 的其它所有情况都是可以这样做的。如果失败了,它将去检查 `core` 标签是否有地址。因为 `core` 标签是必不可少的,如果它没有地址,唯一可能的原因就是它不存在。如果它有地址,那就是我们没有找到我们要找的标签。如果没有找到,那我们就需要查找所有标签的地址。这是通过读取标签编号来做的。如果标签编号为 0,意味着已经到了标签列表的结束位置。这意味着我们已经查找了目录中所有的标签。所以,如果我们再次运行我们的函数,现在它应该能够给出一个答案。如果标签编号不为 0,我们检查这个标签类型是否已经有一个地址。如果没有,我们在目录中保存这个标签的地址。然后增加这个标签的长度(以字节为单位)到标签地址中,然后去查找下一个标签。
|
||||
|
||||
尝试去用汇编实现这段代码。你将需要简化它。如果被卡住了,下面是我的答案。不要忘了 `.section .text`!
|
||||
|
||||
```assembly
|
||||
.section .text
|
||||
.globl FindTag
|
||||
FindTag:
|
||||
tag .req r0
|
||||
tagList .req r1
|
||||
tagAddr .req r2
|
||||
|
||||
sub tag,#1
|
||||
cmp tag,#8
|
||||
movhi tag,#0
|
||||
movhi pc,lr
|
||||
|
||||
ldr tagList,=tag_core
|
||||
tagReturn$:
|
||||
add tagAddr,tagList, tag,lsl #2
|
||||
ldr tagAddr,[tagAddr]
|
||||
|
||||
teq tagAddr,#0
|
||||
movne r0,tagAddr
|
||||
movne pc,lr
|
||||
|
||||
ldr tagAddr,[tagList]
|
||||
teq tagAddr,#0
|
||||
movne r0,#0
|
||||
movne pc,lr
|
||||
|
||||
mov tagAddr,#0x100
|
||||
push {r4}
|
||||
tagIndex .req r3
|
||||
oldAddr .req r4
|
||||
tagLoop$:
|
||||
ldrh tagIndex,[tagAddr,#4]
|
||||
subs tagIndex,#1
|
||||
poplt {r4}
|
||||
blt tagReturn$
|
||||
|
||||
add tagIndex,tagList, tagIndex,lsl #2
|
||||
ldr oldAddr,[tagIndex]
|
||||
teq oldAddr,#0
|
||||
.unreq oldAddr
|
||||
streq tagAddr,[tagIndex]
|
||||
|
||||
ldr tagIndex,[tagAddr]
|
||||
add tagAddr, tagIndex,lsl #2
|
||||
b tagLoop$
|
||||
|
||||
.unreq tag
|
||||
.unreq tagList
|
||||
.unreq tagAddr
|
||||
.unreq tagIndex
|
||||
```
|
||||
|
||||
### 5、Hello World
|
||||
|
||||
现在,我们已经万事俱备了,我们可以去绘制我们的第一个字符串了。在 `main.s` 文件中删除 `bl SetGraphicsAddress` 之后的所有代码,然后将下面的代码放进去:
|
||||
|
||||
```assembly
|
||||
mov r0,#9
|
||||
bl FindTag
|
||||
ldr r1,[r0]
|
||||
lsl r1,#2
|
||||
sub r1,#8
|
||||
add r0,#8
|
||||
mov r2,#0
|
||||
mov r3,#0
|
||||
bl DrawString
|
||||
loop$:
|
||||
b loop$
|
||||
```
|
||||
|
||||
这段代码简单地使用了我们的 `FindTag` 方法去查找第 9 个标签(`cmdline`),然后计算它的长度,然后传递命令和长度给 `DrawString` 方法,告诉它在 `0,0` 处绘制字符串。现在可以在树莓派上测试它了。你应该会在屏幕上看到一行文本。如果没有,请查看我们的排错页面。
|
||||
|
||||
如果一切正常,恭喜你已经能够绘制文本了。但它还有很大的改进空间。如果想去写了一个数字,或内存的一部分,或操作我们的命令行,该怎么做呢?在 [课程 9:屏幕04][2] 中,我们将学习如何操作文本和显示有用的数字和信息。
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
via: https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/screen03.html
|
||||
|
||||
作者:[Alex Chadwick][a]
|
||||
选题:[lujun9972][b]
|
||||
译者:[qhwdw](https://github.com/qhwdw)
|
||||
校对:[校对者ID](https://github.com/校对者ID)
|
||||
|
||||
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
|
||||
|
||||
[a]: https://www.cl.cam.ac.uk
|
||||
[b]: https://github.com/lujun9972
|
||||
[1]: https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/screen02.html
|
||||
[2]: https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/screen04.html
|
Loading…
Reference in New Issue
Block a user