Chinaunix

标题: head.s 汇编几个迷惑 [打印本页]

作者: zylthinking    时间: 2010-08-03 13:49
标题: head.s 汇编几个迷惑
本帖最后由 zylthinking 于 2010-11-08 11:18 编辑

如下代码:

3:
        movl $swapper_pg_dir-__PAGE_OFFSET,%eax
        movl %eax,%cr3                /* set the page table pointer.. */
        movl %cr0,%eax
        orl $0x80000000,%eax
        movl %eax,%cr0                /* ..and set paging (PG) bit */
        jmp 1f                        /* flush the prefetch-queue */
1:
        movl $1f,%eax
        jmp *%eax                /* make sure eip is relocated */
1:
        /* Set up the stack pointer */
        lss stack_start,%esp

现在搞懂了, 不过还有一个问题不明白; 为什么要 jmp 两次, 一般解释是第一次清空预读指令缓存; 但是在绝对跳转之前清空有什么深意吗, 毕竟, jmp *%eax 也会顺带着清空

问题1,为什么 jmp 1f 不能将 eip 从物理地址转换成虚拟地址,  movl $1f,%eax  jmp *%eax 就可以?
是不是编译后,  jmp 1f 中的 1f 被翻译成了相对于当前指令地址的相对偏移, 而 movl $1f,%eax 确实将 lss stack_start,%esp 的绝对地址存入了 eax?

问题2, 3: 1: 1: 这些标号在二进制 binary 中的值到底是什么, 是相对于binary 文件第一个字节的偏移, 还是相对于binary文件text段的第一个字节的偏移, 还是在这个偏移量上又加上 1MB(内核加载基地址)呢

问题3, 从 movl $swapper_pg_dir-__PAGE_OFFSET,%eax 看, 似乎在分页启动之前, $swapper_pg_dir 保存的就已经是 0xc0000000 以上的地址了, 这个地址是怎么得出的呢?
无论从相对于某个起始点的偏移量还是在偏移量上再加上基地址, 似乎都不能得到这个数字。
作者: zylthinking    时间: 2010-08-03 14:00
第 2, 3 个问题我想我明白了, 在编译内核的时候,  0xc0000000 和 1MB 这两个值是都有指定的, 那么所有地址估计在编译器的安排下, 都已经静态赋值成正确的虚拟地址了
作者: snail_314    时间: 2010-08-03 14:08
对。jmp 1f只是offset address jump。只是当前eip加上一个offset,达不到作者想要的目的。
作者: zylthinking    时间: 2010-08-03 14:40
另外, 还有一个问题, CPU 取值令是怎么一个取法, 比如一下指令

.org 0
push ebp
.org 1M
movl esp, ebp

.org 是伪指令, 是不是意味着 .org 不会产生任何机器指令, 那 地址计数器 是怎么变化的呢? 如果说 CPU 取下一条指令时不依赖地址计数器, 那CPU 怎么知道到哪里取下一条指令, 好歹两者之间隔着 1M 呢
作者: zylthinking    时间: 2010-08-03 15:17
对。jmp 1f只是offset address jump。只是当前eip加上一个offset,达不到作者想要的目的。
snail_314 发表于 2010-08-03 14:08



那为什么会有 jump 1f 呢, 据说是清空预读的指令, 问题是 jump 1f 后跟着的是 movl 1f, %eax, 还是物理地址空间, 不会有产生预读?
就算不产生预读, 后边那个 jump *%eax  就不能清空预读指令了吗
作者: etoux    时间: 2010-08-10 14:42
另外, 还有一个问题, CPU 取值令是怎么一个取法, 比如一下指令

.org 0
push ebp
.org 1M
movl esp ...
zylthinking 发表于 2010-08-03 14:40



    这个伪指令是编译前给编译器看的,就是说编译器直接给地址0的地方放 push 0 然后在1m的地址处放 movl esp...  中间插上nop。
个人想法,不对请各位大侠指正。
作者: accessory    时间: 2010-08-11 08:51
LS说的有一个错误。中间不会插上NOP,而是全0。自己写个小程序然后用HEX EDITOR 看下就知道了。

其他是对的。

另外,LZ这段代码是在那个文件里的?也许本身的设计就是让PUSH EBP之后什么都没有而出错。
作者: zylthinking    时间: 2010-11-08 11:19
:em12:
作者: snail_314    时间: 2010-11-08 11:37
回复 1# zylthinking


    这个问题在编译板块有人讨论过了.当时我的看法是第一次jmp清掉刷新pipeline,如果不这么做的话,jmp后面一个指令读操作数阶段仍然是实模式下取得的,所以,这里相当于让后面那个mov在保护模式下重做一次(把前面那次实模式下的pipeline的废掉)
作者: zylthinking    时间: 2010-11-08 11:57
回复  zylthinking


    这个问题在编译板块有人讨论过了.当时我的看法是第一次jmp清掉刷新pipeline,如 ...
snail_314 发表于 2010-11-08 11:37


1f 的值是多少呢, 如果作为一个符号, 那会在生成内核时设成虚拟地址空间的地址, 因此, 无论 缓存线在什么情况下读, 读到的应该都是虚拟地址,那么无论废掉与否, 都应该是正确的才是
作者: snail_314    时间: 2010-11-08 12:31
回复 10# zylthinking


    没说这条跳转

我的意思是,在movl %eax,%cr0    这条指令执行时,因为pipeline的原因,有可能后面x条(x要看pipeline的深度)的操作数已经被取了,但是因为movl %eax,%cr0    还没完成,所以它们的操作数还在实模式下面取得的,自然是不对的(因为后面的指令都希望在保护模式下取数据和写数据).所以这条jmp是clear pipeline,保证后面的x条指令都在保护模式下从取指->解码->取操作数->执行的过程重做一遍
作者: zylthinking    时间: 2010-11-08 12:45
回复  zylthinking


    没说这条跳转

我的意思是,在movl %eax,%cr0    这条指令执行时,因为pipeli ...
snail_314 发表于 2010-11-08 12:31


你说的是general 的情况, 看实际代码可以看到, movl $1f, %cr0 后若干指令为 jump 1f, movl $1f, %eax, jmp *%eax, 在开启保护模式后, eip 在 jmp *%eax 包括 执行  jmp *%eax时是没变的, 还是物理地址取指令; 而涉及到数据段的只有 movl $1f, %eax, 而这个如果 $1f 本身被编译后赋值应该本身就是虚拟地址, 那么即便之前是实模式下缓存的, 反正被缓存的$1f 是虚拟地址, 又有什么关系呢?
作者: snail_314    时间: 2010-11-08 12:52
回复 12# zylthinking


    恩.
并且我刚才说错了,上面代码只涉及了未分页和分页两种情况,和保护模式和实模式无关哈.纠正一下
作者: snail_314    时间: 2010-11-08 13:24
本帖最后由 snail_314 于 2010-11-08 13:46 编辑

回复 12# zylthinking


    我想到一个解释,不知说不说得通

jmp *%eax这条指令在未分页和分页的情况下执行上有区别

pipeline中都是放下面可能会执行的指令的.
未分页情况下cpu会从%eax求值出来的物理地址取指令放到pipeline中(肯定取出来的都是垃圾数据了,毕竟这时%eax大约是0xCxxxxxxx之类的数,这个物理地址下不知道会是些什么咚咚);
分页情况下cpu会从%eax求值出来的数经过mmu再到物理地址取指令放到pipeline中,这才是正确的下一条该执行的指令

依赖cpu的设计,如果pipeline设计得够深,加上什么prefetch等,那么第一种情况有可能会出现,应该是种隐患.
所以,第一条jmp是为了让第一种情况的隐患消失
作者: zylthinking    时间: 2010-11-08 13:38
回复  zylthinking


    我想到一个解释,不知说不说得通

jmp *%eax这条指令在未分页和分页的情况下 ...
snail_314 发表于 2010-11-08 13:24


这个不能靠猜了, 只能找资料或者找高人指点了 呵呵
作者: 奇门遁甲-lu    时间: 2010-11-08 20:38
jmp 1f                        /* flush the prefetch-queue */
这句是多余的。
有jmp *%eax 就够了。
实际中把jmp 1f去掉, 是没有问题的, 实践过。
以前在一个mailist看到过关于arm 也有类似讨论:
是开启mmu 后,因为流水线的关系,要用几个nop来填充。
后来有牛人现身,现代的cpu,已经完全解决了这个问题了,
开启mmu 后立即跳转是没有问题的。
作者: snail_314    时间: 2010-11-08 21:43
那以前的cpu为什么又不行?
作者: 剑魂箫心    时间: 2016-03-06 13:06
本帖最后由 剑魂箫心 于 2016-03-06 18:56 编辑

回复 16# 奇门遁甲-lu


    回答很正确,我看了2.6.8的代码,这个地方已经改成了
  1. 192         movl %eax,%cr0          /* ..and set paging (PG) bit */
  2. 193         ljmp $__BOOT_CS,$1f     /* Clear prefetch and normalize %eip */
  3. 194 1:
  4. 195         /* Set up the stack pointer */
  5. 196         lss stack_start,%esp
复制代码
其中$__BOOT_CS是数字0x10,如果按照selector的格式来看,对应是GDT中index=2的descriptor。
内核编译链接后,标号1处的地址应该是3G+xx。为了方便我们假设它是0xc0100042
这样来说,L193就可以翻译成
  1. ljmp $0x10,$0xc0100042
复制代码
意思就是使用长跳转指令,直接把CS刷成0x10,把eip刷成0xc0100042,这样就完成了把寄存器中的物理地址全改为虚拟地址的过程。在这之后分页机制才会起作用。

总结一下,2.4内核中这个地方写的太罗嗦太晦涩,我做了下实验,将
  1.          jmp 1f /* flush the prefetch-queue */
  2. 1:
复制代码
直接删掉,编译后照样没问题。或者我们借鉴下2.6内核的做法,

  1.          movl %eax,%cr0 /* ..and set paging (PG) bit */
  2.          jmp 1f /* flush the prefetch-queue */
  3. 1:
  4.          movl $1f,%eax
  5.          jmp *%eax /* make sure eip is relocated */
  6. 1:
复制代码
直接改为
  1.          movl %eax,%cr0          /* ..and set paging (PG) bit */
  2.          ljmp $__KERNEL_CS,$1f
  3. 1:
  4.          /* Set up the stack pointer */
复制代码
这样就非常清晰了,我们也用长跳转把CS和EIP一块刷新,经测试,系统编译后可以正常启动。




欢迎光临 Chinaunix (http://bbs.chinaunix.net/) Powered by Discuz! X3.2